Skip to content
Permalink
Browse files Browse the repository at this point in the history
🔒️ Rate limit failed login attempts
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.
  • Loading branch information
foosel committed Aug 15, 2022
1 parent bd51717 commit 82c892b
Show file tree
Hide file tree
Showing 7 changed files with 31 additions and 7 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.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
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.

14 changes: 10 additions & 4 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,15 +29,16 @@ $(function () {
var remember = rememberElement.prop("checked");

overlayElement.addClass("in");
errorElement.removeClass("in");
errorCredentialsElement.removeClass("in");
errorRateElement.removeClass("in");

OctoPrint.browser
.login(username, password, remember)
.done(() => {
ignoreDisconnect = true;
window.location.href = REDIRECT_URL;
})
.fail(() => {
.fail((xhr) => {
usernameElement.val(USER_ID);
passwordElement.val("");

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 82c892b

Please sign in to comment.