Skip to content

Commit 3eccf10

Browse files
committed
Fixe nested <form> elements in dashboard tables causing delete button to fail on the first row
Delete actions reverted from POST forms to GET links with CSRF token passed as URL query parameter CSRF protection preserved via manual `validate_csrf` check in delete endpoints
1 parent dcbe8d4 commit 3eccf10

5 files changed

Lines changed: 48 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to ExaFS will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.2.1] - 2026-01-30
9+
10+
### Fixed
11+
- Fixed nested `<form>` elements in dashboard tables causing delete button to fail on the first row
12+
- Delete actions reverted from POST forms to GET links with CSRF token passed as URL query parameter
13+
- CSRF protection preserved via manual `validate_csrf` check in delete endpoints
14+
815
## [1.2.0] - 2026-01-29
916

1017
### Security

flowapp/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "1.2.0"
1+
__version__ = "1.2.1"
22
__title__ = "ExaFS"
33
__description__ = "Tool for creation, validation, and execution of ExaBGP messages."
44
__author__ = "CESNET / Jiri Vrany, Petr Adamec, Josef Verich, Jakub Man"

flowapp/templates/macros.html

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,9 @@
5252
<a class="btn btn-info btn-sm" href="{{ url_for('rules.reactivate_rule', rule_type=rtype_int, rule_id=rule.id) }}" role="button" data-bs-toggle="tooltip" data-bs-placement="top" title="set expiration">
5353
<i class="bi bi-clock table-icon"></i>
5454
</a>
55-
<form method="POST" action="{{ url_for('rules.delete_rule', rule_type=rtype_int, rule_id=rule.id) }}" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete this rule?');">
56-
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
57-
<button type="submit" class="btn btn-danger btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="delete">
58-
<i class="bi bi-x-lg"></i>
59-
</button>
60-
</form>
55+
<a class="btn btn-danger btn-sm" href="{{ url_for('rules.delete_rule', rule_type=rtype_int, rule_id=rule.id, csrf_token=csrf_token()) }}" role="button" data-bs-toggle="tooltip" data-bs-placement="top" title="delete" onclick="return confirm('Are you sure you want to delete this rule?');">
56+
<i class="bi bi-x-lg"></i>
57+
</a>
6158
{% endif %}
6259
{% if rule.comment %}
6360
<button type="button" class="btn btn-info btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ rule.comment }}">
@@ -111,19 +108,13 @@
111108
<a class="btn btn-info btn-sm" href="{{ url_for('rules.reactivate_rule', rule_type=1, rule_id=rule.id) }}" role="button" data-bs-toggle="tooltip" data-bs-placement="top" title="set expiration">
112109
<i class="bi bi-clock table-icon"></i>
113110
</a>
114-
<form method="POST" action="{{ url_for('rules.delete_rule', rule_type=1, rule_id=rule.id) }}" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete this rule?');">
115-
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
116-
<button type="submit" class="btn btn-danger btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="delete">
117-
<i class="bi bi-x-lg"></i>
118-
</button>
119-
</form>
111+
<a class="btn btn-danger btn-sm" href="{{ url_for('rules.delete_rule', rule_type=1, rule_id=rule.id, csrf_token=csrf_token()) }}" role="button" data-bs-toggle="tooltip" data-bs-placement="top" title="delete" onclick="return confirm('Are you sure you want to delete this rule?');">
112+
<i class="bi bi-x-lg"></i>
113+
</a>
120114
{% if rule.community.id in allowed_communities %}
121-
<form method="POST" action="{{ url_for('rules.delete_and_whitelist', rule_type=1, rule_id=rule.id) }}" style="display:inline;" onsubmit="return confirm('Are you sure you want to whitelist and delete this rule?');">
122-
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
123-
<button type="submit" class="btn btn-success btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="whitelist and delete">
124-
<i class="bi bi-shield-x"></i>
125-
</button>
126-
</form>
115+
<a class="btn btn-success btn-sm" href="{{ url_for('rules.delete_and_whitelist', rule_type=1, rule_id=rule.id, csrf_token=csrf_token()) }}" role="button" data-bs-toggle="tooltip" data-bs-placement="top" title="whitelist and delete" onclick="return confirm('Are you sure you want to whitelist and delete this rule?');">
116+
<i class="bi bi-shield-x"></i>
117+
</a>
127118
{% endif %}
128119
{% endif %}
129120
{% if rule.comment %}
@@ -162,12 +153,9 @@
162153
<a class="btn btn-info btn-sm" href="{{ url_for('whitelist.reactivate', wl_id=rule.id) }}" role="button" data-bs-toggle="tooltip" data-bs-placement="top" title="set expiration">
163154
<i class="bi bi-clock table-icon"></i>
164155
</a>
165-
<form method="POST" action="{{ url_for('whitelist.delete', wl_id=rule.id) }}" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete this whitelist?');">
166-
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
167-
<button type="submit" class="btn btn-danger btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="delete">
168-
<i class="bi bi-x-lg"></i>
169-
</button>
170-
</form>
156+
<a class="btn btn-danger btn-sm" href="{{ url_for('whitelist.delete', wl_id=rule.id, csrf_token=csrf_token()) }}" role="button" data-bs-toggle="tooltip" data-bs-placement="top" title="delete" onclick="return confirm('Are you sure you want to delete this whitelist?');">
157+
<i class="bi bi-x-lg"></i>
158+
</a>
171159
{% endif %}
172160
{% if rule.comment %}
173161
<button type="button" class="btn btn-info btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ rule.comment }}">

flowapp/views/rules.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from collections import namedtuple
44

55
from flask import Blueprint, current_app, flash, redirect, render_template, request, session, url_for
6+
from flask_wtf.csrf import validate_csrf
7+
from wtforms import ValidationError
68

79
from flowapp import constants, db
810
from flowapp.auth import (
@@ -148,7 +150,7 @@ def reactivate_rule(rule_type, rule_id):
148150
)
149151

150152

151-
@rules.route("/delete/<int:rule_type>/<int:rule_id>", methods=["POST"])
153+
@rules.route("/delete/<int:rule_type>/<int:rule_id>", methods=["GET"])
152154
@auth_required
153155
@user_or_admin_required
154156
def delete_rule(rule_type, rule_id):
@@ -157,6 +159,13 @@ def delete_rule(rule_type, rule_id):
157159
:param rule_type: integer - type of rule to be deleted
158160
:param rule_id: integer - rule id
159161
"""
162+
# Validate CSRF token from query parameter
163+
try:
164+
validate_csrf(request.args.get("csrf_token", ""))
165+
except ValidationError:
166+
flash("CSRF token missing or invalid.", "alert-danger")
167+
return redirect(url_for("dashboard.index"))
168+
160169
# Convert the integer rule_type to RuleTypes enum
161170
enum_rule_type = RuleTypes(rule_type)
162171

@@ -205,13 +214,20 @@ def delete_rule(rule_type, rule_id):
205214
)
206215

207216

208-
@rules.route("/delete_and_whitelist/<int:rule_type>/<int:rule_id>", methods=["POST"])
217+
@rules.route("/delete_and_whitelist/<int:rule_type>/<int:rule_id>", methods=["GET"])
209218
@auth_required
210219
@user_or_admin_required
211220
def delete_and_whitelist(rule_type, rule_id):
212221
"""
213222
Delete an RTBH rule and create a whitelist entry from it.
214223
"""
224+
# Validate CSRF token from query parameter
225+
try:
226+
validate_csrf(request.args.get("csrf_token", ""))
227+
except ValidationError:
228+
flash("CSRF token missing or invalid.", "alert-danger")
229+
return redirect(url_for("dashboard.index"))
230+
215231
if rule_type != RuleTypes.RTBH.value:
216232
flash("Only RTBH rules can be converted to whitelists", "alert-warning")
217233
return redirect(url_for("index"))

flowapp/views/whitelist.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from datetime import datetime, timedelta
22
from flask import Blueprint, current_app, flash, redirect, render_template, request, session, url_for
3+
from flask_wtf.csrf import validate_csrf
4+
from wtforms import ValidationError
35

46
from flowapp.auth import (
57
auth_required,
@@ -100,14 +102,21 @@ def reactivate(wl_id):
100102
)
101103

102104

103-
@whitelist.route("/delete/<int:wl_id>", methods=["POST"])
105+
@whitelist.route("/delete/<int:wl_id>", methods=["GET"])
104106
@auth_required
105107
@user_or_admin_required
106108
def delete(wl_id):
107109
"""
108110
Delete whitelist
109111
:param wl_id: integer - id of the whitelist
110112
"""
113+
# Validate CSRF token from query parameter
114+
try:
115+
validate_csrf(request.args.get("csrf_token", ""))
116+
except ValidationError:
117+
flash("CSRF token missing or invalid.", "alert-danger")
118+
return redirect(url_for("dashboard.index"))
119+
111120
# Check if user can modify this whitelist
112121
if check_user_can_modify_rule(wl_id, "whitelist"):
113122
messages = delete_whitelist(wl_id)

0 commit comments

Comments
 (0)