"New Item" template:
{% extends "layout.html" %}
{% block title %}
New item
{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet" type="text/css" media="screen" href="{{ url_for('static', filename='form.css') }}">
{% endblock %}
{% block content %}
<form action="{{ url_for('newItem') }}" method = 'post'>
<h1>Create a new item</h1>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<label>Name</label>
<input type='text' size='30' name='name' placeholder="Name" required>
<label>Description</label>
<textarea rows='4' name='description' placeholder="Description" required></textarea>
<label>Price</label>
<input type='text' size='30' name='price' placeholder="Price" required>
<label>Image URI</label>
<input type='text' size='30' name='image' placeholder="https://example.com/image.png" required>
<label>Category</label>
<select name='category' required>
{% for c in categories %}
<option value="{{ c.id }}">{{ c.name }}</option>
{% endfor %}
</select>
<input type='submit' value='Create'>
<a href="{{ url_for('categories') }}">Cancel</a>
</form>
{% endblock %}
"Login Required" decorator:
def login_required(f): @wraps(f) def decorated_function( * args, ** kwargs): session_cookie = request.cookies.get('session') # Verify the session cookie.In this case an additional check is added to detect # if the user 's Firebase session was revoked, user deleted/disabled, etc. try: decoded_claims = auth.verify_session_cookie(session_cookie, check_revoked = True) return f( * args, ** kwargs) except ValueError as e: # Session cookie is unavailable or invalid.Force user to login. print(e) return redirect(url_for('login', mode = "select", signInSuccessUrl = request.url)) except auth.AuthError as e: # Session revoked.Force user to login. print(e) return redirect(url_for('login', mode = "select", signInSuccessUrl = request.url)) return decorated_function
"Items" endpoint (works as expected):
@app.route('/items/<int:item_id>/')
def item(item_id):
session = DBSession()
item = session.query(Item).get(item_id)
session_cookie = flask.request.cookies.get('session')
# Verify the session cookie. In this case an additional check is added to detect
# if the user's Firebase session was revoked, user deleted/disabled, etc.
try:
auth.verify_session_cookie(session_cookie, check_revoked=True)
return render_template('item.html', item=item, logged=True)
except ValueError as e:
# Session cookie is unavailable or invalid. Force user to login.
print(e)
return render_template('item.html', item=item, logged=False)
except auth.AuthError as e:
# Session revoked. Force user to login.
print(e)
return render_template('item.html', item=item, logged=False)
The error I get on the POST request is the following:
Value Error
Can 't parse segment: \���
GET requests work fine, the server verifies anycodings_python the session cookie on each request and sends anycodings_python content accordingly. But when I do a POST anycodings_python request (submitting a form to create a new anycodings_python item, for example) the server is unable to anycodings_python parse the cookie.,"New Item" endpoint (returns a Set-Cookie anycodings_python header with an invalid cookie to GET anycodings_python requests):,I verified using the Chrome Dev Tools that anycodings_python the session cookie sent to the server on anycodings_python both GET and POST requests are the same. anycodings_python Tried several things I've found googling anycodings_python about similar problems but anything worked. anycodings_python I also tried to find a similar question here anycodings_python but I haven't found any.,To solve the problem I changed the anycodings_flask cookie name from 'session' to any other anycodings_flask name. I don't know why Flask keeps anycodings_flask sending Set-Cookie headers for the anycodings_flask 'session' cookie, so if anyone knows why anycodings_flask please comment it out.
"New Item" template:
{% extends "layout.html" %}
{% block title %}
New item
{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet" type="text/css" media="screen" href="{{ url_for('static', filename='form.css') }}">
{% endblock %}
{% block content %}
<form action="{{ url_for('newItem') }}" method = 'post'>
<h1>Create a new item</h1>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<label>Name</label>
<input type='text' size='30' name='name' placeholder="Name" required>
<label>Description</label>
<textarea rows='4' name='description' placeholder="Description" required></textarea>
<label>Price</label>
<input type='text' size='30' name='price' placeholder="Price" required>
<label>Image URI</label>
<input type='text' size='30' name='image' placeholder="https://example.com/image.png" required>
<label>Category</label>
<select name='category' required>
{% for c in categories %}
<option value="{{ c.id }}">{{ c.name }}</option>
{% endfor %}
</select>
<input type='submit' value='Create'>
<a href="{{ url_for('categories') }}">Cancel</a>
</form>
{% endblock %}
"Login Required" decorator:
def login_required(f): @wraps(f) def decorated_function( * args, ** kwargs): session_cookie = request.cookies.get('session') # Verify the session cookie.In this case an additional check is added to detect # if the user 's Firebase session was revoked, user deleted/disabled, etc. try: decoded_claims = auth.verify_session_cookie(session_cookie, check_revoked = True) return f( * args, ** kwargs) except ValueError as e: # Session cookie is unavailable or invalid.Force user to login. print(e) return redirect(url_for('login', mode = "select", signInSuccessUrl = request.url)) except auth.AuthError as e: # Session revoked.Force user to login. print(e) return redirect(url_for('login', mode = "select", signInSuccessUrl = request.url)) return decorated_function
"Items" endpoint (works as expected):
@app.route('/items/<int:item_id>/')
def item(item_id):
session = DBSession()
item = session.query(Item).get(item_id)
session_cookie = flask.request.cookies.get('session')
# Verify the session cookie. In this case an additional check is added to detect
# if the user's Firebase session was revoked, user deleted/disabled, etc.
try:
auth.verify_session_cookie(session_cookie, check_revoked=True)
return render_template('item.html', item=item, logged=True)
except ValueError as e:
# Session cookie is unavailable or invalid. Force user to login.
print(e)
return render_template('item.html', item=item, logged=False)
except auth.AuthError as e:
# Session revoked. Force user to login.
print(e)
return render_template('item.html', item=item, logged=False)
The error I get on the POST request is the anycodings_python following:
Value Error
Can 't parse segment: \���
When CSRF validation fails, it will raise a CSRFError. By default this returns a response with the failure reason and a 400 code. You can customize the error response using Flask’s errorhandler().,CSRF protection requires a secret key to securely sign the token. By default this will use the Flask app’s SECRET_KEY. If you’d like to use a separate token you can set WTF_CSRF_SECRET_KEY.,In Axios you can set the header for all requests with axios.defaults.headers.common.,When sending an AJAX request, add the X-CSRFToken header to it. For example, in jQuery you can configure all requests to send the token.
from flask_wtf.csrf
import CSRFProtect
csrf = CSRFProtect(app)
csrf = CSRFProtect() def create_app(): app = Flask(__name__) csrf.init_app(app)
<form method="post">
{{ form.csrf_token }}
</form>
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
</form>
<script type="text/javascript">
var csrf_token = "{{ csrf_token() }}";
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrf_token);
}
}
});
</script>
<script type="text/javascript">
axios.defaults.headers.common["X-CSRFToken"] = "{{ csrf_token() }}";
</script>
Consider starting by setting SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS to True. Your application likely doesn’t need ‘login CSRF’ protection, and it is frustrating to not even be able to login via API!,Be aware that for CSRF to work, callers MUST send the session cookie. So for pure API (token based), and no session cookie - there is no way to support ‘login CSRF’. So your app must set SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS (or clients must use CSRF/session cookie for logging in then once they have an authentication token, no further need for cookie).,To protect your application’s endpoints (that presumably are not using Flask forms), you need to enable CSRF as described in the FlaskWTF documentation:,Flask-Security strives to support various options for both its endpoints (e.g. /login) and the application endpoints (protected with Flask-Security decorators such as auth_required()).
Class MyForbiddenException(Exception):
def __init__(self, msg='Not permitted with your privileges', status=http.HTTPStatus.FORBIDDEN):
self.info = {'status': status, 'msgs': [msg]}
_security = app.extensions["security"]
@_security.unauthz_handler
def my_unauthz_handler(func, params):
raise MyForbiddenException()
@app.errorhandler(MyForbiddenException)
def my_exception(ex):
return flask.jsonify(ex.info), ex.info['status']
@app.route('/doc/<int:doc_id>', methods=['PATCH'])
@auth_required('token', 'session')
def doc_patch(doc_id):
doc = fetch_doc(doc_id)
if not current_user.has_role('admin') and doc.owner != current_user:
raise MyForbiddenException(msg='You can only update docs you own')
# This will fetch the csrf - token.Note that we do a GET on the login endpoint # which will get us the csrf - token even though we aren 't yet logged in. # Note further the 'data: null' and explicit Content - Type header - these are # critical, otherwise Flask - Security will return the login form. axios.get('/login', { data: null, headers: { 'Content-Type': 'application/json' } }).then(function(resp) { csrf_token = resp.data['response']['csrf_token'] }) # This will add the token header to each outgoing mutating request. axios.interceptors.request.use(function(config) { if (["post", "delete", "patch", "put"].includes(config["method"])) { if (csrf_token !== '') { config.headers["X-CSRF-Token"] = csrf_token } } return config; }, function(error) { // Do something with request error return Promise.reject(error); });
flask_wtf.CSRFProtect(app)
# Have cookie sent app.config["SECURITY_CSRF_COOKIE_NAME"] = "XSRF-TOKEN" # Don 't have csrf tokens expire (they are invalid after logout) app.config["WTF_CSRF_TIME_LIMIT"] = None # You can 't get the cookie until you are logged in. app.config["SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS"] = True # Enable CSRF protection flask_wtf.CSRFProtect(app)
fetch(url, {
credentials: 'include',
mode: 'cors',
headers: {
'Accept': 'application/json',
'X-XSRF-TOKEN': getCookieValue('XSRF-TOKEN')
}
});
function addUser(details) {
return fetch('https://api.example.com/user', {
mode: 'cors',
method: 'POST',
credentials: 'include',
body: JSON.stringify(details),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-XSRF-TOKEN': getCookieValue('XSRF-TOKEN')
}
}).then(response => {
return response.json().then(data => {
if (response.ok) {
return data;
} else {
return Promise.reject({
status: response.status,
data
});
}
});
});
}
#29 Miguel Grinberg said 2018-12-02T17:09:29Z , #28 Nate said 2018-12-02T10:28:23Z
So to summarize, you want the following two settings added to your production configuration:
SESSION_COOKIE_SECURE = True REMEMBER_COOKIE_SECURE = True
To summarize this section, add the following to both development and production configurations:
SESSION_COOKIE_HTTPONLY = True REMEMBER_COOKIE_HTTPONLY = True
If you are using asynchronous requests (i.e. ajax), you have to manually insert the CSRF token as a custom header in all requests that modify the state of the server, which typically means POST
, PUT
, DELETE
and maybe PATCH
. Here is an example Jinja template from the Flask-WTF documentation that shows how the server passes the CSRF token to the client's JavaScript, and then how the client inserts the custom header using jquery's ajax support:
<script type="text/javascript">
var csrf_token = "{{ csrf_token() }}"; // the token is set by Jinja2
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrf_token); // insert custom header
}
}
});
</script>
Since I consider the session protection support in Flask-Login incomplete, and have been unable to convince the author to improve it, I have now created a separate extension with the specific goal of protecting the session. The extension is called Flask-Paranoid, and you can install it with pip:
pip install flask - paranoid
Once installed, all you need to do is create an instance of the Paranoid()
class and that will protect your session in the same way Flask-Login does in strong mode:
from flask_paranoid
import Paranoid
app = Flask(__name__)
paranoid = Paranoid(app)
paranoid.redirect_view = '/'
Hi, Miguel,
I followed your book to enter the web developing field.Really thanks
for you!
As my understanding, your book mainly describes two scenarios: a) Fully server side rendering technology based on Jinja.b) Restful API
for a smart client, such as android app.
Both of the two scenarios are clear
for the development of the server logic.Your book is enough
for me now.
But now I face another scenario
for me(maybe it is not special thing
for you), browser < - > vuejs fontend < - > flask restful api.
I totally confused to do the security relative things even though I read plenty articles till now.I have selected flask - jwt - extended as the basic authentication utility.It supports token embedding cookie with httponly setting.So I understand this technology immune to XSS attack.Now I don 't know how to defense CSRF attack in my scenario. I don'
t
know
if I have to use flask - seasurf extension.All of the post forms will be generated by vuejs, so no flask - wtf will be used.And because there is no page will be rendered by flask, so how to insert the csrf - token to the page which is controlled by vuejs.
@urbainy: You can
return the CSRF token in a cookie, which then the client needs to copy into a custom header.This method is called the double submit cookie, it is documented here: https: //www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Double_Submit_Cookie.
Hi @Miguel I installed Flask - Paranoid to cover a potential vulnerability in my software(which it does), however the downside is that when toggling between web - and responsive - mode the refresh logs the user out.I can see that this is a good thing from the module 's perspective, however my potential client-base are obsessed with UAT, and will often toggle between different platform simulations to compare behaviour. It 's probably a really simple thing, but can you suggest a way that I might allow testers to disable Paranoid while testing? (I' m using an app factory and initialising paranoid with "paranoid.init_app(app)") I considered either a per - user basis, or environment - wide(obviously I 'd also be communicating that this option will not be available in a Prod environment, and should not be used when running penetration testing)... but to be honest, I can' t find anything about temporary disabling modules... Cheers
@Nate: can you add a setting in flask.config ? Depending on the value of the setting you initialize flask - paranoid or not.
Great summary Miguel.Question on session IDs that I can’ t seem to find in the flask - login documentation.Are these automatically regenerated on login / logout(
for added security) ?
On the successful login, the server response includes the Set-Cookie header that contains the cookie name, value, expiry time and some other info. Here is an example that sets the cookie named JSESSIONID: Set-Cookie: JSESSIONID=abcde12345; Path=/; HttpOnly ,On the logout operation, the server sends back the Set-Cookie header that causes the cookie to expire.,The client needs to send this cookie in the Cookie header in all subsequent requests to the server. Cookie: JSESSIONID=abcde12345 ,Note that the Set-Cookie header and securitySchemes are not connected in any way, and the Set-Header definition is for documentation purposes only.
JSESSIONID
Set - Cookie: JSESSIONID = abcde12345;
Path = /; HttpOnly
Cookie
Cookie: JSESSIONID = abcde12345
JSESSIONID
Set - Cookie: JSESSIONID = abcde12345;
Path = /; HttpOnly
Cookie
Cookie: JSESSIONID = abcde12345
security
paths:
/users:
get:
security:
-cookieAuth: []
description: Returns a list of users.
responses:
'200':
description: OK
So it appears the session cookie is not being set in Edge - when you navigate to the login page. I checked another Flask app that I have and it is being set for that app, so there must be some config issue on this app.,None of those are likely to cause the issue. Are you sure that the session cookie is being sent to Edge? If it is, then Edge should have some sort of indication somewhere about why it rejected the cookie.,Sure -- another idea is to use developers tools in Edge and check if it refuses to send the cookie for some reason (Chrome would provide some information in such a case).,I finally fixed the issue. For me I added the below to my config to ensure domain was not being set in the set-cookie directive.
from flask_wtf.csrf
import CSRFProtect
csrf = CSRFProtect()
csrf.init_app(app)
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="csrf_token" value="IjdhMGM5MjAyMGViZWI0ZjkxM2U2ZjQwOGI2YWI1YTI5ZmNiZjZmYTYi.YLZA9Q.fkFgK5gN6x0LryFCqbwHLIjKxTg"/>
SERVER_NAME = 'flaskcms.pythonanywhere.com'
SESSION_COOKIE_DOMAIN = False