mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-04-30 18:22:38 +02:00
Add CSRF validation to most disruptive actions
This also switches all such actions to POST requests. Deleting the project is handled in another commit because it requires more changes.
This commit is contained in:
parent
942617a436
commit
109d7fca17
7 changed files with 44 additions and 12 deletions
|
@ -373,3 +373,9 @@ class InviteForm(FlaskForm):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("The email %(email)s is not valid", email=email)
|
_("The email %(email)s is not valid", email=email)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyForm(FlaskForm):
|
||||||
|
"""Used for CSRF validation"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
|
@ -313,7 +313,7 @@ footer .footer-left {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bill-actions > .delete,
|
.bill-actions > form > .delete,
|
||||||
.bill-actions > .edit,
|
.bill-actions > .edit,
|
||||||
.bill-actions > .show {
|
.bill-actions > .show {
|
||||||
font-size: 0px;
|
font-size: 0px;
|
||||||
|
@ -323,9 +323,10 @@ footer .footer-left {
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
float: left;
|
float: left;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bill-actions > .delete {
|
.bill-actions > form > .delete {
|
||||||
background: url("../images/delete.png") no-repeat right;
|
background: url("../images/delete.png") no-repeat right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -131,7 +131,10 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="bill-actions">
|
<td class="bill-actions">
|
||||||
<a class="edit" href="{{ url_for(".edit_bill", bill_id=bill.id) }}" title="{{ _("edit") }}">{{ _('edit') }}</a>
|
<a class="edit" href="{{ url_for(".edit_bill", bill_id=bill.id) }}" title="{{ _("edit") }}">{{ _('edit') }}</a>
|
||||||
<a class="delete" href="{{ url_for(".delete_bill", bill_id=bill.id) }}" title="{{ _("delete") }}">{{ _('delete') }}</a>
|
<form action="{{ url_for(".delete_bill", bill_id=bill.id) }}" method="POST">
|
||||||
|
{{ csrf_form.csrf_token }}
|
||||||
|
<button class="action delete" type="submit" title="{{ _("delete") }}"></button>
|
||||||
|
</form>
|
||||||
{% if bill.external_link %}
|
{% if bill.external_link %}
|
||||||
<a class="show" href="{{ bill.external_link }}" ref="noopener" target="_blank" title="{{ _("show") }}">{{ _('show') }} </a>
|
<a class="show" href="{{ bill.external_link }}" ref="noopener" target="_blank" title="{{ _("show") }}">{{ _('show') }} </a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
{%- if member.activated %}
|
{%- if member.activated %}
|
||||||
<td>
|
<td>
|
||||||
<form class="action delete" action="{{ url_for(".remove_member", member_id=member.id) }}" method="POST">
|
<form class="action delete" action="{{ url_for(".remove_member", member_id=member.id) }}" method="POST">
|
||||||
|
{{ csrf_form.csrf_token }}
|
||||||
<button type="submit">{{ _("deactivate") }}</button>
|
<button type="submit">{{ _("deactivate") }}</button>
|
||||||
</form>
|
</form>
|
||||||
<form class="action edit" action="{{ url_for(".edit_member", member_id=member.id) }}" method="GET">
|
<form class="action edit" action="{{ url_for(".edit_member", member_id=member.id) }}" method="GET">
|
||||||
|
@ -31,6 +32,7 @@
|
||||||
{%- else %}
|
{%- else %}
|
||||||
<td>
|
<td>
|
||||||
<form class="action reactivate" action="{{ url_for(".reactivate", member_id=member.id) }}" method="POST">
|
<form class="action reactivate" action="{{ url_for(".reactivate", member_id=member.id) }}" method="POST">
|
||||||
|
{{ csrf_form.csrf_token }}
|
||||||
<button type="submit">{{ _("reactivate") }}</button>
|
<button type="submit">{{ _("reactivate") }}</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -550,7 +550,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
self.assertEqual(bill.amount, 10, "bill edition")
|
self.assertEqual(bill.amount, 10, "bill edition")
|
||||||
|
|
||||||
# delete the bill
|
# delete the bill
|
||||||
self.client.get(f"/raclette/delete/{bill.id}")
|
self.client.post(f"/raclette/delete/{bill.id}")
|
||||||
self.assertEqual(0, len(models.Bill.query.all()), "bill deletion")
|
self.assertEqual(0, len(models.Bill.query.all()), "bill deletion")
|
||||||
|
|
||||||
# test balance
|
# test balance
|
||||||
|
@ -1375,10 +1375,10 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
resp = self.client.post("/tartiflette/edit/1", data=modified_bill)
|
resp = self.client.post("/tartiflette/edit/1", data=modified_bill)
|
||||||
self.assertStatus(404, resp)
|
self.assertStatus(404, resp)
|
||||||
# Try to delete bill
|
# Try to delete bill
|
||||||
resp = self.client.get("/raclette/delete/1")
|
resp = self.client.post("/raclette/delete/1")
|
||||||
self.assertStatus(303, resp)
|
self.assertStatus(303, resp)
|
||||||
# Try to delete bill by ID
|
# Try to delete bill by ID
|
||||||
resp = self.client.get("/tartiflette/delete/1")
|
resp = self.client.post("/tartiflette/delete/1")
|
||||||
self.assertStatus(302, resp)
|
self.assertStatus(302, resp)
|
||||||
|
|
||||||
# Additional check that the bill was indeed not modified or deleted
|
# Additional check that the bill was indeed not modified or deleted
|
||||||
|
@ -1396,7 +1396,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
bill = models.Bill.query.filter(models.Bill.id == 1).one_or_none()
|
bill = models.Bill.query.filter(models.Bill.id == 1).one_or_none()
|
||||||
self.assertNotEqual(bill, None, "bill not found")
|
self.assertNotEqual(bill, None, "bill not found")
|
||||||
self.assertEqual(bill.what, "roblochon")
|
self.assertEqual(bill.what, "roblochon")
|
||||||
self.client.get("/raclette/delete/1")
|
self.client.post("/raclette/delete/1")
|
||||||
bill = models.Bill.query.filter(models.Bill.id == 1).one_or_none()
|
bill = models.Bill.query.filter(models.Bill.id == 1).one_or_none()
|
||||||
self.assertEqual(bill, None)
|
self.assertEqual(bill, None)
|
||||||
|
|
||||||
|
|
|
@ -222,7 +222,7 @@ class HistoryTestCase(IhatemoneyTestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
# delete the bill
|
# delete the bill
|
||||||
resp = self.client.get(f"/demo/delete/{bill_id}", follow_redirects=True)
|
resp = self.client.post(f"/demo/delete/{bill_id}", follow_redirects=True)
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
# delete user using POST method
|
# delete user using POST method
|
||||||
|
@ -389,7 +389,7 @@ class HistoryTestCase(IhatemoneyTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
# delete the bill
|
# delete the bill
|
||||||
resp = self.client.get("/demo/delete/1", follow_redirects=True)
|
resp = self.client.post("/demo/delete/1", follow_redirects=True)
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
resp = self.client.get("/demo/history")
|
resp = self.client.get("/demo/history")
|
||||||
|
@ -527,7 +527,7 @@ class HistoryTestCase(IhatemoneyTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
# delete the bill
|
# delete the bill
|
||||||
self.client.get("/demo/delete/1", follow_redirects=True)
|
self.client.post("/demo/delete/1", follow_redirects=True)
|
||||||
|
|
||||||
resp = self.client.get("/demo/history")
|
resp = self.client.get("/demo/history")
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
|
@ -40,6 +40,7 @@ from ihatemoney.forms import (
|
||||||
AdminAuthenticationForm,
|
AdminAuthenticationForm,
|
||||||
AuthenticationForm,
|
AuthenticationForm,
|
||||||
EditProjectForm,
|
EditProjectForm,
|
||||||
|
EmptyForm,
|
||||||
InviteForm,
|
InviteForm,
|
||||||
MemberForm,
|
MemberForm,
|
||||||
PasswordReminder,
|
PasswordReminder,
|
||||||
|
@ -608,6 +609,7 @@ def invite():
|
||||||
@main.route("/<project_id>/")
|
@main.route("/<project_id>/")
|
||||||
def list_bills():
|
def list_bills():
|
||||||
bill_form = get_billform_for(g.project)
|
bill_form = get_billform_for(g.project)
|
||||||
|
csrf_form = EmptyForm()
|
||||||
# set the last selected payer as default choice if exists
|
# set the last selected payer as default choice if exists
|
||||||
if "last_selected_payer" in session:
|
if "last_selected_payer" in session:
|
||||||
bill_form.payer.data = session["last_selected_payer"]
|
bill_form.payer.data = session["last_selected_payer"]
|
||||||
|
@ -623,6 +625,7 @@ def list_bills():
|
||||||
bills=bills,
|
bills=bills,
|
||||||
member_form=MemberForm(g.project),
|
member_form=MemberForm(g.project),
|
||||||
bill_form=bill_form,
|
bill_form=bill_form,
|
||||||
|
csrf_form=csrf_form,
|
||||||
add_bill=request.values.get("add_bill", False),
|
add_bill=request.values.get("add_bill", False),
|
||||||
current_view="list_bills",
|
current_view="list_bills",
|
||||||
)
|
)
|
||||||
|
@ -644,6 +647,12 @@ def add_member():
|
||||||
|
|
||||||
@main.route("/<project_id>/members/<member_id>/reactivate", methods=["POST"])
|
@main.route("/<project_id>/members/<member_id>/reactivate", methods=["POST"])
|
||||||
def reactivate(member_id):
|
def reactivate(member_id):
|
||||||
|
# Used for CSRF validation
|
||||||
|
form = EmptyForm()
|
||||||
|
if not form.validate():
|
||||||
|
flash(_("CSRF Token: The CSRF token is invalid."), category="danger")
|
||||||
|
return redirect(url_for(".list_bills"))
|
||||||
|
|
||||||
person = (
|
person = (
|
||||||
Person.query.filter(Person.id == member_id)
|
Person.query.filter(Person.id == member_id)
|
||||||
.filter(Project.id == g.project.id)
|
.filter(Project.id == g.project.id)
|
||||||
|
@ -658,6 +667,12 @@ def reactivate(member_id):
|
||||||
|
|
||||||
@main.route("/<project_id>/members/<member_id>/delete", methods=["POST"])
|
@main.route("/<project_id>/members/<member_id>/delete", methods=["POST"])
|
||||||
def remove_member(member_id):
|
def remove_member(member_id):
|
||||||
|
# Used for CSRF validation
|
||||||
|
form = EmptyForm()
|
||||||
|
if not form.validate():
|
||||||
|
flash(_("CSRF Token: The CSRF token is invalid."), category="danger")
|
||||||
|
return redirect(url_for(".list_bills"))
|
||||||
|
|
||||||
member = g.project.remove_member(member_id)
|
member = g.project.remove_member(member_id)
|
||||||
if member:
|
if member:
|
||||||
if not member.activated:
|
if not member.activated:
|
||||||
|
@ -715,9 +730,14 @@ def add_bill():
|
||||||
return render_template("add_bill.html", form=form)
|
return render_template("add_bill.html", form=form)
|
||||||
|
|
||||||
|
|
||||||
@main.route("/<project_id>/delete/<int:bill_id>")
|
@main.route("/<project_id>/delete/<int:bill_id>", methods=["POST"])
|
||||||
def delete_bill(bill_id):
|
def delete_bill(bill_id):
|
||||||
# fixme: everyone is able to delete a bill
|
# Used for CSRF validation
|
||||||
|
form = EmptyForm()
|
||||||
|
if not form.validate():
|
||||||
|
flash(_("CSRF Token: The CSRF token is invalid."), category="danger")
|
||||||
|
return redirect(url_for(".list_bills"))
|
||||||
|
|
||||||
bill = Bill.query.get(g.project, bill_id)
|
bill = Bill.query.get(g.project, bill_id)
|
||||||
if not bill:
|
if not bill:
|
||||||
return redirect(url_for(".list_bills"))
|
return redirect(url_for(".list_bills"))
|
||||||
|
|
Loading…
Reference in a new issue