Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to ExaFS will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.2.1] - 2026-01-30

### Fixed
- Fixed 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.2.0] - 2026-01-29

### Security
Expand Down
2 changes: 1 addition & 1 deletion flowapp/__about__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "1.2.0"
__version__ = "1.2.1"
__title__ = "ExaFS"
__description__ = "Tool for creation, validation, and execution of ExaBGP messages."
__author__ = "CESNET / Jiri Vrany, Petr Adamec, Josef Verich, Jakub Man"
Expand Down
36 changes: 12 additions & 24 deletions flowapp/templates/macros.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,9 @@
<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">
<i class="bi bi-clock table-icon"></i>
</a>
<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?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-danger btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="delete">
<i class="bi bi-x-lg"></i>
</button>
</form>
<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?');">
<i class="bi bi-x-lg"></i>
</a>
{% endif %}
{% if rule.comment %}
<button type="button" class="btn btn-info btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ rule.comment }}">
Expand Down Expand Up @@ -111,19 +108,13 @@
<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">
<i class="bi bi-clock table-icon"></i>
</a>
<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?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-danger btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="delete">
<i class="bi bi-x-lg"></i>
</button>
</form>
<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?');">
<i class="bi bi-x-lg"></i>
</a>
{% if rule.community.id in allowed_communities %}
<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?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-success btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="whitelist and delete">
<i class="bi bi-shield-x"></i>
</button>
</form>
<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?');">
<i class="bi bi-shield-x"></i>
</a>
{% endif %}
{% endif %}
{% if rule.comment %}
Expand Down Expand Up @@ -162,12 +153,9 @@
<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">
<i class="bi bi-clock table-icon"></i>
</a>
<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?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-danger btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="delete">
<i class="bi bi-x-lg"></i>
</button>
</form>
<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?');">
<i class="bi bi-x-lg"></i>
</a>
{% endif %}
{% if rule.comment %}
<button type="button" class="btn btn-info btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ rule.comment }}">
Expand Down
20 changes: 18 additions & 2 deletions flowapp/views/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from collections import namedtuple

from flask import Blueprint, current_app, flash, redirect, render_template, request, session, url_for
from flask_wtf.csrf import validate_csrf
from wtforms import ValidationError

from flowapp import constants, db
from flowapp.auth import (
Expand Down Expand Up @@ -148,7 +150,7 @@ def reactivate_rule(rule_type, rule_id):
)


@rules.route("/delete/<int:rule_type>/<int:rule_id>", methods=["POST"])
@rules.route("/delete/<int:rule_type>/<int:rule_id>", methods=["GET"])
@auth_required
@user_or_admin_required
def delete_rule(rule_type, rule_id):
Expand All @@ -157,6 +159,13 @@ def delete_rule(rule_type, rule_id):
:param rule_type: integer - type of rule to be deleted
:param rule_id: integer - rule id
"""
# Validate CSRF token from query parameter
try:
validate_csrf(request.args.get("csrf_token", ""))
except ValidationError:
flash("CSRF token missing or invalid.", "alert-danger")
return redirect(url_for("dashboard.index"))

# Convert the integer rule_type to RuleTypes enum
enum_rule_type = RuleTypes(rule_type)

Expand Down Expand Up @@ -205,13 +214,20 @@ def delete_rule(rule_type, rule_id):
)


@rules.route("/delete_and_whitelist/<int:rule_type>/<int:rule_id>", methods=["POST"])
@rules.route("/delete_and_whitelist/<int:rule_type>/<int:rule_id>", methods=["GET"])
@auth_required
@user_or_admin_required
def delete_and_whitelist(rule_type, rule_id):
"""
Delete an RTBH rule and create a whitelist entry from it.
"""
# Validate CSRF token from query parameter
try:
validate_csrf(request.args.get("csrf_token", ""))
except ValidationError:
flash("CSRF token missing or invalid.", "alert-danger")
return redirect(url_for("dashboard.index"))

if rule_type != RuleTypes.RTBH.value:
flash("Only RTBH rules can be converted to whitelists", "alert-warning")
return redirect(url_for("index"))
Expand Down
11 changes: 10 additions & 1 deletion flowapp/views/whitelist.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from datetime import datetime, timedelta
from flask import Blueprint, current_app, flash, redirect, render_template, request, session, url_for
from flask_wtf.csrf import validate_csrf
from wtforms import ValidationError

from flowapp.auth import (
auth_required,
Expand Down Expand Up @@ -100,14 +102,21 @@ def reactivate(wl_id):
)


@whitelist.route("/delete/<int:wl_id>", methods=["POST"])
@whitelist.route("/delete/<int:wl_id>", methods=["GET"])
@auth_required
@user_or_admin_required
def delete(wl_id):
"""
Delete whitelist
:param wl_id: integer - id of the whitelist
"""
# Validate CSRF token from query parameter
try:
validate_csrf(request.args.get("csrf_token", ""))
except ValidationError:
flash("CSRF token missing or invalid.", "alert-danger")
return redirect(url_for("dashboard.index"))

# Check if user can modify this whitelist
if check_user_can_modify_rule(wl_id, "whitelist"):
messages = delete_whitelist(wl_id)
Expand Down