Fix: Add CSRF validation to api key management page (#1523)
* Fix: Add CSRF validation to api key management page * Added csrf to subdomain creation * Added CSRF to totp cancel Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
This commit is contained in:
parent
0ab53ad49a
commit
d874acfe2c
|
@ -7,6 +7,7 @@ from app.dashboard.base import dashboard_bp
|
||||||
from app.dashboard.views.enter_sudo import sudo_required
|
from app.dashboard.views.enter_sudo import sudo_required
|
||||||
from app.db import Session
|
from app.db import Session
|
||||||
from app.models import ApiKey
|
from app.models import ApiKey
|
||||||
|
from app.utils import CSRFValidationForm
|
||||||
|
|
||||||
|
|
||||||
class NewApiKeyForm(FlaskForm):
|
class NewApiKeyForm(FlaskForm):
|
||||||
|
@ -23,9 +24,13 @@ def api_key():
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
csrf_form = CSRFValidationForm()
|
||||||
new_api_key_form = NewApiKeyForm()
|
new_api_key_form = NewApiKeyForm()
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
if not csrf_form.validate():
|
||||||
|
flash("Invalid request", "warning")
|
||||||
|
return redirect(request.url)
|
||||||
if request.form.get("form-name") == "delete":
|
if request.form.get("form-name") == "delete":
|
||||||
api_key_id = request.form.get("api-key-id")
|
api_key_id = request.form.get("api-key-id")
|
||||||
|
|
||||||
|
@ -62,5 +67,8 @@ def api_key():
|
||||||
return redirect(url_for("dashboard.api_key"))
|
return redirect(url_for("dashboard.api_key"))
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"dashboard/api_key.html", api_keys=api_keys, new_api_key_form=new_api_key_form
|
"dashboard/api_key.html",
|
||||||
|
api_keys=api_keys,
|
||||||
|
new_api_key_form=new_api_key_form,
|
||||||
|
csrf_form=csrf_form,
|
||||||
)
|
)
|
||||||
|
|
|
@ -34,7 +34,7 @@ def batch_import_route():
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if not csrf_form.validate():
|
if not csrf_form.validate():
|
||||||
flash("Invalid request", "warning")
|
flash("Invalid request", "warning")
|
||||||
redirect(request.url)
|
return redirect(request.url)
|
||||||
if len(batch_imports) > 10:
|
if len(batch_imports) > 10:
|
||||||
flash(
|
flash(
|
||||||
"You have too many imports already. Wait until some get cleaned up",
|
"You have too many imports already. Wait until some get cleaned up",
|
||||||
|
|
|
@ -5,6 +5,7 @@ from app.dashboard.base import dashboard_bp
|
||||||
from app.dashboard.views.enter_sudo import sudo_required
|
from app.dashboard.views.enter_sudo import sudo_required
|
||||||
from app.db import Session
|
from app.db import Session
|
||||||
from app.models import RecoveryCode
|
from app.models import RecoveryCode
|
||||||
|
from app.utils import CSRFValidationForm
|
||||||
|
|
||||||
|
|
||||||
@dashboard_bp.route("/mfa_cancel", methods=["GET", "POST"])
|
@dashboard_bp.route("/mfa_cancel", methods=["GET", "POST"])
|
||||||
|
@ -15,8 +16,13 @@ def mfa_cancel():
|
||||||
flash("you don't have MFA enabled", "warning")
|
flash("you don't have MFA enabled", "warning")
|
||||||
return redirect(url_for("dashboard.index"))
|
return redirect(url_for("dashboard.index"))
|
||||||
|
|
||||||
|
csrf_form = CSRFValidationForm()
|
||||||
|
|
||||||
# user cancels TOTP
|
# user cancels TOTP
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
if not csrf_form.validate():
|
||||||
|
flash("Invalid request", "warning")
|
||||||
|
return redirect(request.url)
|
||||||
current_user.enable_otp = False
|
current_user.enable_otp = False
|
||||||
current_user.otp_secret = None
|
current_user.otp_secret = None
|
||||||
Session.commit()
|
Session.commit()
|
||||||
|
@ -28,4 +34,4 @@ def mfa_cancel():
|
||||||
flash("TOTP is now disabled", "warning")
|
flash("TOTP is now disabled", "warning")
|
||||||
return redirect(url_for("dashboard.index"))
|
return redirect(url_for("dashboard.index"))
|
||||||
|
|
||||||
return render_template("dashboard/mfa_cancel.html")
|
return render_template("dashboard/mfa_cancel.html", csrf_form=csrf_form)
|
||||||
|
|
|
@ -2,6 +2,8 @@ import re
|
||||||
|
|
||||||
from flask import render_template, request, redirect, url_for, flash
|
from flask import render_template, request, redirect, url_for, flash
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, validators
|
||||||
|
|
||||||
from app import parallel_limiter
|
from app import parallel_limiter
|
||||||
from app.config import MAX_NB_SUBDOMAIN
|
from app.config import MAX_NB_SUBDOMAIN
|
||||||
|
@ -14,6 +16,15 @@ from app.models import CustomDomain, Mailbox, SLDomain
|
||||||
_SUBDOMAIN_PATTERN = r"[0-9a-z-]{1,}"
|
_SUBDOMAIN_PATTERN = r"[0-9a-z-]{1,}"
|
||||||
|
|
||||||
|
|
||||||
|
class NewSubdomainForm(FlaskForm):
|
||||||
|
domain = StringField(
|
||||||
|
"domain", validators=[validators.DataRequired(), validators.Length(max=64)]
|
||||||
|
)
|
||||||
|
subdomain = StringField(
|
||||||
|
"subdomain", validators=[validators.DataRequired(), validators.Length(max=64)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dashboard_bp.route("/subdomain", methods=["GET", "POST"])
|
@dashboard_bp.route("/subdomain", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@parallel_limiter.lock(only_when=lambda: request.method == "POST")
|
@parallel_limiter.lock(only_when=lambda: request.method == "POST")
|
||||||
|
@ -28,9 +39,13 @@ def subdomain_route():
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
errors = {}
|
errors = {}
|
||||||
|
new_subdomain_form = NewSubdomainForm()
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if request.form.get("form-name") == "create":
|
if request.form.get("form-name") == "create":
|
||||||
|
if not new_subdomain_form.validate():
|
||||||
|
flash("Invalid new subdomain", "warning")
|
||||||
|
return redirect(url_for("dashboard.subdomain_route"))
|
||||||
if not current_user.is_premium():
|
if not current_user.is_premium():
|
||||||
flash("Only premium plan can add subdomain", "warning")
|
flash("Only premium plan can add subdomain", "warning")
|
||||||
return redirect(request.url)
|
return redirect(request.url)
|
||||||
|
@ -41,8 +56,8 @@ def subdomain_route():
|
||||||
)
|
)
|
||||||
return redirect(request.url)
|
return redirect(request.url)
|
||||||
|
|
||||||
subdomain = request.form.get("subdomain").lower().strip()
|
subdomain = new_subdomain_form.subdomain.data.lower().strip()
|
||||||
domain = request.form.get("domain").lower().strip()
|
domain = new_subdomain_form.domain.data.lower().strip()
|
||||||
|
|
||||||
if len(subdomain) < 3:
|
if len(subdomain) < 3:
|
||||||
flash("Subdomain must have at least 3 characters", "error")
|
flash("Subdomain must have at least 3 characters", "error")
|
||||||
|
@ -110,4 +125,5 @@ def subdomain_route():
|
||||||
sl_domains=sl_domains,
|
sl_domains=sl_domains,
|
||||||
errors=errors,
|
errors=errors,
|
||||||
subdomains=subdomains,
|
subdomains=subdomains,
|
||||||
|
new_subdomain_form=new_subdomain_form,
|
||||||
)
|
)
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
{{ csrf_form.csrf_token }}
|
||||||
<input type="hidden" name="form-name" value="delete">
|
<input type="hidden" name="form-name" value="delete">
|
||||||
<input type="hidden" name="api-key-id" value="{{ api_key.id }}">
|
<input type="hidden" name="api-key-id" value="{{ api_key.id }}">
|
||||||
<span class="card-link btn btn-link float-right text-danger delete-api-key">Delete</span>
|
<span class="card-link btn btn-link float-right text-danger delete-api-key">Delete</span>
|
||||||
|
@ -57,6 +58,7 @@
|
||||||
{% if api_keys|length > 0 %}
|
{% if api_keys|length > 0 %}
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
{{ csrf_form.csrf_token }}
|
||||||
<input type="hidden" name="form-name" value="delete-all">
|
<input type="hidden" name="form-name" value="delete-all">
|
||||||
<span class="delete btn btn-outline-danger delete-all-api-keys float-right">
|
<span class="delete btn btn-outline-danger delete-all-api-keys float-right">
|
||||||
Delete All <i class="fe fe-trash"></i>
|
Delete All <i class="fe fe-trash"></i>
|
||||||
|
@ -66,7 +68,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<hr />
|
<hr />
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{{ new_api_key_form.csrf_token }}
|
{{ csrf_form.csrf_token }}
|
||||||
<input type="hidden" name="form-name" value="create">
|
<input type="hidden" name="form-name" value="create">
|
||||||
<h2 class="h4">New API Key</h2>
|
<h2 class="h4">New API Key</h2>
|
||||||
{{ new_api_key_form.name(class="form-control", placeholder="Chrome") }}
|
{{ new_api_key_form.name(class="form-control", placeholder="Chrome") }}
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
or use WebAuthn (FIDO).
|
or use WebAuthn (FIDO).
|
||||||
</div>
|
</div>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
{{ csrf_form.csrf_token }}
|
||||||
<button class="btn btn-danger mt-2">Disable TOTP</button>
|
<button class="btn btn-danger mt-2">Disable TOTP</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -72,6 +72,7 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="h4 mb-1">New Subdomain</h2>
|
<h2 class="h4 mb-1">New Subdomain</h2>
|
||||||
<form method="post" class="mt-2" data-parsley-validate>
|
<form method="post" class="mt-2" data-parsley-validate>
|
||||||
|
{{ new_subdomain_form.csrf_token }}
|
||||||
<input type="hidden" name="form-name" value="create">
|
<input type="hidden" name="form-name" value="create">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Subdomain</label>
|
<label>Subdomain</label>
|
||||||
|
|
Loading…
Reference in New Issue