Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Flask, how do I prevent a route from being accessed unless another route has been visited first?

Tags:

python

flask

PROBLEM STATEMENT

I'm working on a Flask web app that displays a list of items in a table. The user can select a row and hit a Delete button to delete the item. However, before the item is deleted from the database, the user is first routed to a confirmation screen where some item details are displayed as well as a Confirm button. The url for the confirmation page follows this pattern: dashboard/confirm-delete/<id> and the url for the actual delete page follows this pattern: dashboard/delete/<id>. See admin/views.py below for more details.

While the system works, the problem I have is that a user can simply skip the confirmation page by typing dashboard/delete/<id>, where <id> is substituted by an actual item id, into the address bar.

QUESTIONS

Is there a way to prevent users from accessing dashboard/delete/<id> unless they first go to dashboard/confirm-delete/<id> (the confirmation screen)? Alternatively, is my approach wrong and is there a better one available?

CURRENT CODE:

Function in my dashboard.html page called when a row is selected and the delete button is pressed:

$('#remove').click(function () {
  var id = getId();
  window.location.href="/dashboard/confirm-delete" + $.trim(id);
  });

Confirm button in confirm-delete.html (the delete confirmation page):

<a class="btn btn-default" href="{{ url_for('admin.delete_item', id=item.id) }}" role="button">Confirm Delete</a> 

My admins/views.py:

@admin_blueprint.route('dashboard/confirm-delete/<id>')
@login_required
@groups_required(['admin'})
def confirm_delete_item(id)
  item = Item.query.get_or_404(id)
  return render_template('admin/confirm-delete.html', item=item, title="Delete Item")

@admin_blueprint.route('dashboard/delete/<id>', methods=['GET', 'POST'])
@login_required
@groups_required(['admin'})
def delete_item(id)
  item = Item.query.get_or_404(id)
  db.session.delete(item)
  db.commit()
  return redirect(url_for('home.homepage'))

SOLUTION

Based on the answer marked as accepted I solved the problem as follows:

First, I created a new form to handle the Submit button in the confirm-delete.html page:

admin/forms.py:

from flask_wtf import FlaskForm
from wtforms import SubmitField

class DeleteForm(FlaskForm):
  submit = SubmitField('Confirm')

I substituted the Confirm Button code with the following to confirm-delete.html:

<form method="post">
  {{ form.csrf_token }}
  {{ form.submit }}
</form>

Finally, I merged both of the functions in app/views.py as follows:

@admin_blueprint.route('dashboard/confirm-delete/<id>', methods=['GET', 'POST'])
@login_required
@groups_required(['admin'})
def confirm_delete_item(id)
  form = DeleteForm()
  item = Item.query.get_or_404(id)
  if form.validate_on_submit():  
    if form.submit.data:
      db.session.delete(item)
      db.commit()
      return redirect(url_for('home.homepage'))
  return render_template('admin/confirm-delete.html', item=item, form=form, title="Delete Item")

This way, a user can't bypass the delete confirmation screen by typing a specific link in the address bar, plus it simplifies the code.

like image 494
nessus_pp Avatar asked Oct 18 '22 16:10

nessus_pp


2 Answers

As already mentioned in comments, one way of solving your problem is checking for a certain cookie as the user sends a request. But personally I would not recommend this method, because such cookies can very likely be compromised unless you come up with some sort of hashing algorithm to hash the cookie values and check them in some way.
To my mind, the most easy, secure and natural way of doing it is protecting /delete route with CSRF-token. You can implement it with Flask_WTF extension.
In a word, you have to create something like DeleteForm, then you put {{form.csrf_token}} in your confirm-delete.htmland validate it in delete_view() with form.validate_on_submit()

Check out their docs:
http://flask-wtf.readthedocs.io/en/stable/form.html
http://flask-wtf.readthedocs.io/en/stable/csrf.html

like image 56
mister_potato Avatar answered Oct 21 '22 09:10

mister_potato


I would make the delete page POST-only. The browser may skip a GET request or try it many times, you have no control over it. A crawler could follow an anonymous delete link and delete all your wiki articles. A browser prefetcher could prefetch a logout link.

REST purists would insist you use GET, POST, DELETE and PUT methods for their intended purposes.

https://softwareengineering.stackexchange.com/questions/188860/why-shouldnt-a-get-request-change-data-on-the-server

So,

In HTML

<form action='/dashboard/delete/{{id}}' method='post'>

In Flask

@app.route('/dashboard/delete/<int:id>', methods=['POST'])
def delete(id):
like image 45
Jesvin Jose Avatar answered Oct 21 '22 08:10

Jesvin Jose