Repository: Mailu/Mailu

Issue:
Login attempt on roundcube triggers error 500 on /sso/login endpoint (ZeroDivisionError: division by zero)
Hi everybody!

Thanks in advance for your help!

## Before you open your issue
- [x] Check if no issue or pull-request for this already exists.
- [x] Check [documentation](https://mailu.io/master/) and [FAQ](https://mailu.io/master/faq.html). (Tip, use the search function on the documentation page)
- [x] You understand `Mailu` is made by volunteers in their **free time** — be conscise, civil and accept that delays can occur.
- [x] The title of the issue should be short and simple. It should contain specific terms related to the actual issue. Be specific while writing the title.

## Environment & Versions
### Environment
 - [x] docker-compose
 - [ ] kubernetes
 - [ ] docker swarm

### Versions
1.9

## Description
A user encountered an error 500 while trying to log into an account on roundcube. The 500 comes from a ZeroDivisionError in one of the jinja templates (see logs below).

## Replication Steps
Unfortunately I could not reproduce it so far. Apparently it happened on the first login attempt, even though I suspect that it had to do with rate limiting, since there was a message about rate limiting right before the error (see below). Apparently the number of fields on the sso form is zero?

The user also reports, that it is now working (also when rapidly logging out and logging in again).

## Expected behaviour
No error 500 and succesful login.

## Logs

````markdown
```
admin_1     | [2022-01-10 14:59:49,322] WARNING in limiter: Authentication attempt from <REDACTED IP OF USER> for <REDACTED MAIL ACCOUNT> has been rate-limited.
admin_1     | [2022-01-10 14:59:49,334] ERROR in app: Exception on /sso/login [POST]
admin_1     | Traceback (most recent call last):
admin_1     |   File "/usr/lib/python3.9/site-packages/flask/app.py", line 2073, in wsgi_app
admin_1     |     response = self.full_dispatch_request()
admin_1     |   File "/usr/lib/python3.9/site-packages/flask/app.py", line 1518, in full_dispatch_request
admin_1     |     rv = self.handle_user_exception(e)
admin_1     |   File "/usr/lib/python3.9/site-packages/flask/app.py", line 1516, in full_dispatch_request
admin_1     |     rv = self.dispatch_request()
admin_1     |   File "/usr/lib/python3.9/site-packages/flask/app.py", line 1502, in dispatch_request
admin_1     |     return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
admin_1     |   File "/app/mailu/sso/views/base.py", line 36, in login
admin_1     |     return flask.render_template('login.html', form=form)
admin_1     |   File "/usr/lib/python3.9/site-packages/flask/templating.py", line 147, in render_template
admin_1     |     return _render(
admin_1     |   File "/usr/lib/python3.9/site-packages/flask/templating.py", line 128, in _render
admin_1     |     rv = template.render(context)
admin_1     |   File "/usr/lib/python3.9/site-packages/jinja2/environment.py", line 1304, in render
admin_1     |     self.environment.handle_exception()
admin_1     |   File "/usr/lib/python3.9/site-packages/jinja2/environment.py", line 925, in handle_exception
admin_1     |     raise rewrite_traceback_stack(source=source)
admin_1     |   File "/app/mailu/sso/templates/login.html", line 1, in top-level template code
admin_1     |     {%- extends "form_sso.html" %}
admin_1     |   File "/app/mailu/sso/templates/form_sso.html", line 1, in top-level template code
admin_1     |     {%- extends "base_sso.html" %}
admin_1     |   File "/app/mailu/sso/templates/base_sso.html", line 70, in top-level template code
admin_1     |     {%- block content %}{%- endblock %}
admin_1     |   File "/app/mailu/sso/templates/form_sso.html", line 4, in block 'content'
admin_1     |     {%- call macros.card() %}
admin_1     |   File "/usr/lib/python3.9/site-packages/jinja2/runtime.py", line 828, in _invoke
admin_1     |     rv = self._func(*arguments)
admin_1     |   File "/app/mailu/ui/templates/macros.html", line 84, in template
admin_1     |     {{- caller() }}
admin_1     |   File "/usr/lib/python3.9/site-packages/jinja2/runtime.py", line 828, in _invoke
admin_1     |     rv = self._func(*arguments)
admin_1     |   File "/app/mailu/sso/templates/form_sso.html", line 8, in template
admin_1     |     {{ macros.form_fields(fields, label=False, class="btn btn-default") }}
admin_1     |   File "/usr/lib/python3.9/site-packages/jinja2/runtime.py", line 828, in _invoke
admin_1     |     rv = self._func(*arguments)
admin_1     |   File "/app/mailu/ui/templates/macros.html", line 22, in template
admin_1     |     {%- set width = (12 / fields|length)|int %}
admin_1     | ZeroDivisionError: division by zero

```
````



Discussion:
The stack trace indeed indicates the error comes from ` `` {%- set width = (12 / fields|length)|int %}` ` ` 

A possible replication scenario could maybe be:
1). Set a low value for AUTH_RATELIMIT_USER or AUTH_RATELIMIT_IP
2). Trigger the rate limiter by failing to login multiple times. 
3). Check the logs if we see the division by zero error.

I think I see the problem in the stack trace and source code.
https://github.com/Mailu/Mailu/blob/e3e370018791e17d647e014c6543e5b6ee79a23f/core/admin/mailu/sso/views/base.py#L33
https://github.com/Mailu/Mailu/blob/e3e370018791e17d647e014c6543e5b6ee79a23f/core/admin/mailu/sso/views/base.py#L36

I think these render_template calls miss the fields variable. This causes the fields variable to be empty (0) and results in the division by zero error.
https://github.com/Mailu/Mailu/blob/e3e370018791e17d647e014c6543e5b6ee79a23f/core/admin/mailu/sso/views/base.py#L49

Resolving patch:
diff --git a/core/admin/mailu/sso/views/base.py b/core/admin/mailu/sso/views/base.py
--- a/core/admin/mailu/sso/views/base.py
+++ b/core/admin/mailu/sso/views/base.py
@@ -30,10 +30,10 @@ def login():
         username = form.email.data
         if username != device_cookie_username and utils.limiter.should_rate_limit_ip(client_ip):
             flask.flash('Too many attempts from your IP (rate-limit)', 'error')
-            return flask.render_template('login.html', form=form)
+            return flask.render_template('login.html', form=form, fields=fields)
         if utils.limiter.should_rate_limit_user(username, client_ip, device_cookie, device_cookie_username):
             flask.flash('Too many attempts for this user (rate-limit)', 'error')
-            return flask.render_template('login.html', form=form)
+            return flask.render_template('login.html', form=form, fields=fields)
         user = models.User.login(username, form.pw.data)
         if user:
             flask.session.regenerate()


Test patch: