Skip to content
Open
59 changes: 54 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
24 changes: 20 additions & 4 deletions marketplace/__init__.py
Original file line number Diff line number Diff line change
@@ -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!'
return app
37 changes: 37 additions & 0 deletions marketplace/db.py
Original file line number Diff line number Diff line change
@@ -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)
34 changes: 34 additions & 0 deletions marketplace/listings.py
Original file line number Diff line number Diff line change
@@ -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')

8 changes: 8 additions & 0 deletions marketplace/schema.sql
Original file line number Diff line number Diff line change
@@ -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)
);
9 changes: 9 additions & 0 deletions marketplace/templates/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<title>{% block title %}{% endblock %} - Secure Coding Python</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<section class="content">
<header>
{% block header %}{% endblock %}
</header>
{% block content %}{% endblock %}
</section>
15 changes: 15 additions & 0 deletions marketplace/templates/listings/create.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% extends 'base.html' %}

{% block header %}
<h1>{% block title %}Create new listings{% endblock %}</h1>
{% endblock %}

{% block content %}
<form method="post">
<label for="title">Title</label>
<input name="title" id="title" required>
<label for="description">Description</label>
<input type="description" name="description" id="description" required>
<input type="submit" value="Create">
</form>
{% endblock %}
21 changes: 21 additions & 0 deletions marketplace/templates/listings/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{% extends 'base.html' %}

{% block header %}
<h1>{% block title %}Listings{% endblock %}</h1>
{% endblock %}

{% block content %}
{% for listing in listings %}
<article class="listing">
<header>
<div>
<h1>{{ listing[1] }}</h1>
</div>
</header>
<p class="body">{{ listing[2] }}</p>
</article>
{% if not loop.last %}
<hr>
{% endif %}
{% endfor %}
{% endblock %}
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
Flask==1.0.3
safety==1.8.5
psycopg2==2.8.3