Store transfer tokens hashed in the db and only allow them to be valid for 24 hours (#1080)

* Store transfer tokens hashed in the db and only allow them to be valid for 30 mins

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
This commit is contained in:
Adrià Casajús 2022-06-13 12:41:47 +02:00 committed by GitHub
parent 91b3e05ed6
commit efa534fd3e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 105 additions and 26 deletions

View file

@ -453,6 +453,9 @@ if len(VERP_EMAIL_SECRET) < 32:
raise RuntimeError(
"Please, set VERP_EMAIL_SECRET to a random string at least 32 chars long"
)
ALIAS_TRANSFER_TOKEN_SECRET = os.environ.get("ALIAS_TRANSFER_TOKEN_SECRET") or (
FLASK_SECRET + "aliastransfertoken"
)
def get_allowed_redirect_domains() -> List[str]:

View file

@ -1,9 +1,12 @@
from uuid import uuid4
import base64
import hmac
import secrets
import arrow
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user
from app.config import URL
from app import config
from app.dashboard.base import dashboard_bp
from app.dashboard.views.enter_sudo import sudo_required
from app.db import Session
@ -76,6 +79,15 @@ def transfer(alias, new_user, new_mailboxes: [Mailbox]):
Session.commit()
def hmac_alias_transfer_token(transfer_token: str) -> str:
alias_hmac = hmac.new(
config.ALIAS_TRANSFER_TOKEN_SECRET.encode("utf-8"),
transfer_token.encode("utf-8"),
"sha3_224",
)
return base64.urlsafe_b64encode(alias_hmac.digest()).decode("utf-8").rstrip("=")
@dashboard_bp.route("/alias_transfer/send/<int:alias_id>/", methods=["GET", "POST"])
@login_required
@sudo_required
@ -92,37 +104,35 @@ def alias_transfer_send_route(alias_id):
)
return redirect(url_for("dashboard.index"))
if alias.transfer_token:
alias_transfer_url = (
URL + "/dashboard/alias_transfer/receive" + f"?token={alias.transfer_token}"
)
else:
alias_transfer_url = None
alias_transfer_url = None
# generate a new transfer_token
if request.method == "POST":
# generate a new transfer_token
if request.form.get("form-name") == "create":
alias.transfer_token = str(uuid4())
transfer_token = f"{alias.id}.{secrets.token_urlsafe(32)}"
alias.transfer_token = hmac_alias_transfer_token(transfer_token)
alias.transfer_token_expiration = arrow.utcnow().shift(hours=24)
Session.commit()
alias_transfer_url = (
URL
config.URL
+ "/dashboard/alias_transfer/receive"
+ f"?token={alias.transfer_token}"
+ f"?token={transfer_token}"
)
flash("Share URL created", "success")
return redirect(request.url)
flash("Share alias URL created", "success")
# request.form.get("form-name") == "remove"
else:
alias.transfer_token = None
alias.transfer_token_expiration = None
Session.commit()
alias_transfer_url = None
flash("Share URL deleted", "success")
return redirect(request.url)
return render_template(
"dashboard/alias_transfer_send.html",
alias=alias,
alias_transfer_url=alias_transfer_url,
link_active=alias.transfer_token_expiration is not None
and alias.transfer_token_expiration > arrow.utcnow(),
)
@ -134,12 +144,27 @@ def alias_transfer_receive_route():
URL has ?alias_id=signed_alias_id
"""
token = request.args.get("token")
alias = Alias.get_by(transfer_token=token)
if not token:
flash("Invalid transfer token", "error")
return redirect(url_for("dashboard.index"))
hashed_token = hmac_alias_transfer_token(token)
# TODO: Don't allow unhashed tokens once all the tokens have been migrated to the new format
alias = Alias.get_by(transfer_token=token) or Alias.get_by(
transfer_token=hashed_token
)
if not alias:
flash("Invalid link", "error")
return redirect(url_for("dashboard.index"))
# TODO: Don't allow none once all the tokens have been migrated to the new format
if (
alias.transfer_token_expiration is not None
and alias.transfer_token_expiration < arrow.utcnow()
):
flash("Expired link, please request a new one", "error")
return redirect(url_for("dashboard.index"))
# alias already belongs to this user
if alias.user_id == current_user.id:
flash("You already own this alias", "warning")
@ -176,11 +201,12 @@ def alias_transfer_receive_route():
return redirect(request.url)
LOG.d(
"transfer alias %s from %s to %s with %s",
"transfer alias %s from %s to %s with %s with token %s",
alias,
alias.user,
current_user,
mailboxes,
token,
)
transfer(alias, current_user, mailboxes)
flash(f"You are now owner of {alias.email}", "success")

View file

@ -1286,6 +1286,9 @@ class Alias(Base, ModelMixin):
# used to transfer an alias to another user
transfer_token = sa.Column(sa.String(64), default=None, unique=True, nullable=True)
transfer_token_expiration = sa.Column(
ArrowType, default=arrow.utcnow, nullable=True
)
# have I been pwned
hibp_last_check = sa.Column(ArrowType, default=None)

View file

@ -0,0 +1,29 @@
"""Add alias transfer token expiration
Revision ID: a7bcb872c12a
Revises: 36646e5dc6d9
Create Date: 2022-06-13 10:29:39.614171
"""
import sqlalchemy_utils
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a7bcb872c12a'
down_revision = '36646e5dc6d9'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('alias', sa.Column('transfer_token_expiration', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('alias', 'transfer_token_expiration')
# ### end Alembic commands ###

View file

@ -16,10 +16,6 @@
emails.
</p>
<p>
In order to transfer ownership,
please create the <b>Share URL</b> 👇 and send it to the other person.
</p>
{% if alias_transfer_url %}
<em data-toggle="tooltip"
@ -29,18 +25,40 @@
{{ alias_transfer_url }}
</em>
<p class="mt-5">
Please copy the transfer URL. <strong>We won't be able to display it again</strong>. If you need to access it again you can generate a new URL.
</p>
<p class="mt-2">
This transfer URL is <strong>valid for 24 hours</strong>. If it hasn't been used by then it will be automatically disabled.
</p>
<form method="post">
<input type="hidden" name="form-name" value="remove">
<button class="btn btn-warning mt-2">Remove Share URL</button>
<button class="btn btn-warning mt-2">Remove alias transfer URL</button>
<div class="small-text">
If you don't want to share this alias anymore, you can remove the share URL.
</div>
</form>
{% else %}
<form method="post">
<input type="hidden" name="form-name" value="create">
<button class="btn btn-primary mt-2">Create Share URL</button>
</form>
{% if link_active %}
<p class="alert alert-info">
You have an active transfer link. If you don't want to share this alias any more, please delete the link.
</p>
<form method="post">
<input type="hidden" name="form-name" value="remove">
<button class="btn btn-warning mt-2">Remove alias transfer URL</button>
</form>
{% else %}
<p>
In order to transfer ownership,
please create the <b>Share URL</b> 👇 and send it to the other person.
</p>
<form method="post">
<input type="hidden" name="form-name" value="create">
<button class="btn btn-primary mt-2">Generate a new alias transfer URL</button>
</form>
{% endif %}
{% endif %}
<p class="mt-5">