From e8fee583faf146be3e75b82d8fd176b559c07bc6 Mon Sep 17 00:00:00 2001 From: Nicolas Valcarcel Date: Thu, 12 Sep 2019 00:10:28 -0500 Subject: [PATCH 1/2] use a random key for the sessions --- README.md | 42 ++++++------------------- marketplace/models.py | 8 +++++ marketplace/users.py | 14 ++++++--- migrations/versions/0697265799f2_.py | 46 ++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 38 deletions(-) create mode 100644 migrations/versions/0697265799f2_.py diff --git a/README.md b/README.md index 106efb3..8ea21ff 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,17 @@ # Secure Coding with Python. ## Chapter 5: Broken De-Authentication -### Test -To test this we are going to make use of probably the most essential tool that web security professionals use: -[Burp Suite](https://portswigger.net/burp). For the purposes of this course we are only going to use the community -edition. +### Fix +In order to avoid sessions to be used even after the user has logged out, we should use a random unique value in the +session that we could revoke on logout, invalidating the session. -1. Please download and install Burp Community Edition. -2. Run Burp Suite. It will give you some options for creating or opening a project. -3. Select `Temporary project` as all we need and the only one allowed for the community edition. -4. Click `Next`. -5. Select `Use Burp defaults` on the configuration page. -6. Click `Start Burp`. -7. Go to the `Proxy` tab on Burp. -8. Select the `Options` sub-tab. -9. Configure your browser to use the proxy settings from `Proxy Listeners`. **Note**: Chrome will ignore proxy request on localhost, the use of Firefox is recommended. -10. Go to the `Intercept` sub-tab. -11. Make sure `Intercept is off` (it's usually on by default, we will enable it later.) -12. Navigate to [http://localhost:5000/user/login](http://localhost:5000/user/login) -13. Login with the credentials of the user you created. -14. On `Burp` go to the sub-tab `HTTP history`. -15. Find the `/user/welcome` request. -16. On the bottom half under `Request` -> `Raw` you can see the cookie being set like `Cookie: session=eyJsb2dnZWRfaW4iOnRydWV9.XXnIiQ.U46jDCKmFDSH-b4_0FiyiBhNMqQ` -17. Copy the cookie value. -18. On the web app click `Logout`. -19. In `Proxy` `Intercept` turn `Intercept is on`. -20. Navigate to [http://localhost:5000/user/welcome](http://localhost:5000/user/welcome) -21. In `Proxy` `Intercept` `Params` change the cookie value to the one we copied on step 17. -22. Click `Forward`. +Since we are adding a new column to our user model we need to update our Database with: +```bash +> $ flask db migrate +> $ flask db upgrade +``` -As you can see even after the user logged out, we were able to log in using the session value captured previously -successfully performing a session hijacking attack. - -**Note**: At the moment of this writing the latest Burp Suite Community Edition version is v2.1.02 - - -**Proceed to [next section](https://github.com/nxvl/secure-coding-with-python/tree/5.1-broken-deauthentication/fix)** +**Proceed to [next section](https://github.com/nxvl/secure-coding-with-python/tree/5.2-broken-deauthentication/code)** ## Index ### 1. Vulnerable Components diff --git a/marketplace/models.py b/marketplace/models.py index d160222..7915f95 100644 --- a/marketplace/models.py +++ b/marketplace/models.py @@ -1,3 +1,5 @@ +from secrets import token_urlsafe + import bcrypt from sqlalchemy.ext.hybrid import hybrid_property @@ -10,6 +12,7 @@ class User(db.Model): full_name = db.Column(db.String(100)) email = db.Column(db.String(100), unique=True) _password = db.Column('password', db.String(100)) + session_key = db.Column('session_key', db.String(50), unique=True) @hybrid_property def password(self): @@ -20,6 +23,11 @@ def password(self, plaintext): salt = bcrypt.gensalt(rounds=12) self._password = bcrypt.hashpw(plaintext.encode(), salt).decode() + def new_session_key(self): + key = token_urlsafe() + self.session_key = key + return self.session_key + class Listing(db.Model): __tablename__ = 'listings' diff --git a/marketplace/users.py b/marketplace/users.py index 1fe8115..709607c 100644 --- a/marketplace/users.py +++ b/marketplace/users.py @@ -1,5 +1,3 @@ -from base64 import b64encode - import bcrypt from flask import Blueprint, request, render_template, session, url_for, redirect @@ -32,7 +30,8 @@ def login(): if u: password = request.form['password'] if bcrypt.checkpw(password.encode(), u.password.encode()): - session['logged_in'] = True + session['key'] = u.new_session_key() + db.session.commit() return redirect(url_for('users.welcome')) error = "Invalid email or password." @@ -41,13 +40,18 @@ def login(): @bp.route('/logout', methods=('GET',)) def logout(): - session['logged_in'] = False + if session.get('key'): + key = session.pop('key') + u = db.session.query(User).filter_by(session_key=key).scalar() + u.new_session_key() + db.session.commit() return redirect(url_for('users.login')) @bp.route('/welcome', methods=('GET',)) def welcome(): - if session.get('logged_in'): + key = session.get('key') + if key and db.session.query(User).filter_by(session_key=key).scalar(): return render_template('users/welcome.html') else: return redirect(url_for('users.login')) diff --git a/migrations/versions/0697265799f2_.py b/migrations/versions/0697265799f2_.py new file mode 100644 index 0000000..9aed1d0 --- /dev/null +++ b/migrations/versions/0697265799f2_.py @@ -0,0 +1,46 @@ +"""empty message + +Revision ID: 0697265799f2 +Revises: 67168ab4efaa +Create Date: 2019-09-11 23:49:27.582749 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0697265799f2' +down_revision = '67168ab4efaa' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('listings', 'description', + existing_type=sa.VARCHAR(length=500), + nullable=True) + op.alter_column('listings', 'title', + existing_type=sa.VARCHAR(length=128), + nullable=True) + op.add_column('users', sa.Column('session_key', sa.String(length=50), nullable=True)) + op.create_unique_constraint(None, 'users', ['session_key']) + op.create_unique_constraint(None, 'users', ['email']) + op.drop_column('users', 'verified') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('verified', sa.BOOLEAN(), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'users', type_='unique') + op.drop_constraint(None, 'users', type_='unique') + op.drop_column('users', 'session_key') + op.alter_column('listings', 'title', + existing_type=sa.VARCHAR(length=128), + nullable=False) + op.alter_column('listings', 'description', + existing_type=sa.VARCHAR(length=500), + nullable=False) + # ### end Alembic commands ### From 1929f1a7973ee93099cb0b59947d3cdd5309e900 Mon Sep 17 00:00:00 2001 From: Nicolas Valcarcel Date: Fri, 13 Sep 2019 21:59:25 -0500 Subject: [PATCH 2/2] update logout function --- marketplace/users.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/marketplace/users.py b/marketplace/users.py index b33a9a3..b8abe48 100644 --- a/marketplace/users.py +++ b/marketplace/users.py @@ -40,16 +40,15 @@ def login(): @bp.route('/logout', methods=('GET',)) -def logout(): - if session.get('key'): - key = session.pop('key') - u = db.session.query(User).filter_by(session_key=key).scalar() - u.new_session_key() - db.session.commit() +@auth +def logout(user): + session.pop('key') + user.new_session_key() + db.session.commit() return redirect(url_for('users.login')) @bp.route('/welcome', methods=('GET',)) @auth -def welcome(): +def welcome(user): return render_template('users/welcome.html')