{{ listing[1] }}
+{{ listing[2] }}
+diff --git a/README.md b/README.md index 4607087..46e8245 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,62 @@ # Secure Coding with Python. -## Chapter 1: Project Bootstrap -### Fix -In this case the fix is extremely simple, we just need up upgrade Flask to 1.0.3 in the `requirements.txt` file and run: +## Chapter 2: SQL Injection +### Requirement +For our marketplace application, we first decide to allow the upload of Listings, just text. We will +worry about users later, since we want to focus on getting the DB and Models setup without needed to worry about +authentication and session management at this point. + +### Setting up the DB +First we need to install postgresql, you can do that with your preferred package manager, for this tutorial we will be +using postgres 11.4. After installing you would probably need to init the db: +```bash +> initdb /usr/local/var/postgres +``` +*Note*: on linux you might need to run the command as root or use sudo. + +Then we create the `marketplace` database: +```bash +> createdb marketplace +``` +*Note*: on linux you might need to run the command as postgres user by prepending `sudo -u postgres` to the command. + +Then we need to install the python driver for python, which comes as the `psycopg2` package. +```bash +> pip install psycopg2 +``` +or ```bash -> pip install -r requirements.txt --upgrade +> pip install -r requirements.txt ``` +*Note*: On OSX if you installed postgres from homebrew, you might need to prepend +`LDFLAGS="-I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib"` to the command in order to install correctly. + +### Development +Since the application will need some more configuration we change the `marketplace/__init__.py` to make use of the +`create_app` factory function. We add the DB connection functions into `marketplace/db.py` and add the factory function. +We also add the DB schema in `schema.sql` and add a flask command to init the DB, which we run with: +```bash +> python -m flask init-db +``` + +### Vulnerability +Since we are generating the SQL to insert the new listing in a very unsecure way, we can insert SQL commands that will +be run in the DB. For example if we insert `'` as title or description we will get +`psycopg2.errors.SyntaxError: INSERT has more target columns than expressions LINE 1: INSERT INTO listings (title, description) VALUES (''', ''') ^` +instead of a success. + +We can for example get the postgresql version or any other SQL function result, to check that out, insert +`injection', (select version()))-- -` as the title. When we do so, the SQL that's going to be executed will be the +following: + +```sql +INSERT INTO listings (title, description) VALUES ('injection', (select version()))-- -', 'ignored description') +``` + +As it can be seen, the inserted title will be `injection` and the description will be the result of the +`select version()` command, or any other command we wish to insert there, including dropping the DB. -**Proceed to [next section](https://github.com/nxvl/secure-coding-with-python/tree/2.1-sql-injection/code)** +**Proceed to [next section](https://github.com/nxvl/secure-coding-with-python/tree/2.1-sql-injection/test)** ## Index ### 1. Vulnerable Components diff --git a/marketplace/__init__.py b/marketplace/__init__.py index 1b4f4af..a23df9c 100644 --- a/marketplace/__init__.py +++ b/marketplace/__init__.py @@ -1,7 +1,23 @@ +import os + from flask import Flask -app = Flask(__name__) +def create_app(test_config=None): + app = Flask(__name__, instance_relative_config=True) + app.config.from_mapping( + SECRET_KEY='dev', + DATABASE='marketplace', + ) + + try: + os.makedirs(app.instance_path) + except OSError: + pass + + from . import db + db.init_app(app) + + from . import listings + app.register_blueprint(listings.bp) -@app.route('/') -def hello(): - return 'Hello, World!' \ No newline at end of file + return app \ No newline at end of file diff --git a/marketplace/db.py b/marketplace/db.py new file mode 100644 index 0000000..b60ee14 --- /dev/null +++ b/marketplace/db.py @@ -0,0 +1,37 @@ +import psycopg2 + +import click +from flask import current_app, g +from flask.cli import with_appcontext + +def get_db(): + if 'db' not in g: + g.db = psycopg2.connect(dbname=current_app.config['DATABASE']) + + return g.db + +def close_db(e=None): + db = g.pop('db', None) + + if db is not None: + db.close() + +def init_db(): + db = get_db() + cur = db.cursor() + + with current_app.open_resource('schema.sql') as f: + cur.execute(f.read().decode('utf8')) + db.commit() + + +@click.command('init-db') +@with_appcontext +def init_db_command(): + """Clear the existing data and create new tables.""" + init_db() + click.echo('Initialized the database.') + +def init_app(app): + app.teardown_appcontext(close_db) + app.cli.add_command(init_db_command) \ No newline at end of file diff --git a/marketplace/listings.py b/marketplace/listings.py new file mode 100644 index 0000000..3f9dbcb --- /dev/null +++ b/marketplace/listings.py @@ -0,0 +1,34 @@ +import sys + +from flask import Blueprint, request, redirect, render_template, url_for + +from marketplace.db import get_db + +bp = Blueprint('listings', __name__, url_prefix='/listings') + +@bp.route('/') +def index(): + cur = get_db().cursor() + cur.execute( + 'SELECT id, title, description' + ' FROM listings' + ) + listings = cur.fetchall() + return render_template('listings/index.html', listings=listings) + +@bp.route('/create', methods=('GET', 'POST')) +def register(): + if request.method == 'POST': + title = request.form['title'] + description = request.form['description'] + db = get_db() + cur = db.cursor() + + sql = "INSERT INTO listings (title, description) VALUES ('%s', '%s')" % (title, description) + print(sql, file=sys.stdout) + cur.execute(sql) + db.commit() + return redirect(url_for('listings.index')) + + return render_template('listings/create.html') + diff --git a/marketplace/schema.sql b/marketplace/schema.sql new file mode 100644 index 0000000..f9d122d --- /dev/null +++ b/marketplace/schema.sql @@ -0,0 +1,8 @@ +DROP TABLE IF EXISTS listings; + +CREATE TABLE listings( + id SERIAL NOT NULL, + title VARCHAR(128) NOT NULL, + description VARCHAR(500) NOT NULL, + PRIMARY KEY (id) +); \ No newline at end of file diff --git a/marketplace/templates/base.html b/marketplace/templates/base.html new file mode 100644 index 0000000..c6e57df --- /dev/null +++ b/marketplace/templates/base.html @@ -0,0 +1,9 @@ + +
{{ listing[2] }}
+