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.
  • 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.