git clone https://github.com/sehnryr/secure-coding-project.git
cd secure-coding-project
docker compose up --build
Each module needs to be adequately implemented to pass the learning outcome.
Use tools OWASP ZAP and Burp Suite to scan your application for vulnerabilities. Choose three most significant vulnerabilities, document them, fix them and re-scan the application to prove that the vulnerabilities are fixed.
When running the OWASP ZAP scan, the following significant vulnerabilities were found:
- Cross Site Scripting (Reflected)
- SQL Injection - MySQL
- Server Side Template Injection
Cross Site Scripting (Reflected) and Server Side Template Injection were fixed
by escaping the user input in the /hello
route.
# From
@app.route('/hello', methods=['GET'])
def hello():
name = request.args.get('name', 'World')
return render_template_string("<h1>Hello, {}!</h1>".format(name))
# To
@app.route('/hello', methods=['GET'])
def hello():
name = request.args.get('name', 'World')
return render_template_string("<h1>Hello, {{ name }}!</h1>", name=name)
SQL Injection was fixed by using SQLAlchemy's ORM to interact with the database.
# From
@app.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = db.session.execute(text(f"SELECT * FROM user WHERE username='{username}' AND password='{password}'")).fetchone()
if user:
return jsonify(msg='Login Successful')
return 'Login Failed', 401
...
# To
@app.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = User.query.filter_by(username=username, password=password).first()
if user:
return jsonify(msg='Login Successful')
return 'Login Failed', 401
...
Use SonarQube tool and scan your application for the bugs. Choose three most significant bugs, document them, fix them and re-scan the application to prove that the bugs are fixed.
Since we've used SonarQube only in Maven projects during the labs, I've followed the instructions here: https://dev.to/mbaoma/sonarqube-as-code-health-checker-for-flask-project-3h94.
Once the setup was done, the command sonar-scanner
was run to scan the
project.
sonar-scanner \
-Dsonar.projectKey=SonarQube---Project \
-Dsonar.sources=. \
-Dsonar.host.url=http://localhost:9000 \
-Dsonar.token=sqp_d8dc603256b62a9a97d2c025090baa62629002d9
The following issues were found:
- Don't disclose "Flask" secret keys. (L14)
- Revoke and change this password, as it is compromised. (L29)
The two issues were fixed by removing the secret key from the code and changing the admin credentials using environment variables.
# From
app = Flask(__name__)
app.config['SECRET_KEY'] = 'supersecretkey'
app.config['SQLALCHEMY_DATABASE_URI'] = f'mysql+pymysql://{db_user}:{db_password}@{db_host}/{db_name}'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
...
def init_db():
with app.app_context():
db.create_all()
if not User.query.filter_by(username='admin').first():
admin = User(username='admin', password='admin')
db.session.add(admin)
db.session.commit()
# To
flask_secret_key = environ.get('FLASK_SECRET')
admin_username = environ.get('ADMIN_USERNAME')
admin_password = environ.get('ADMIN_PASSWORD')
app = Flask(__name__)
app.config['SECRET_KEY'] = flask_secret_key
app.config['SQLALCHEMY_DATABASE_URI'] = f'mysql+pymysql://{db_user}:{db_password}@{db_host}/{db_name}'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
...
def init_db():
with app.app_context():
db.create_all()
if not User.query.filter_by(username=admin_username).first():
admin = User(username=admin_username, password=admin_password)
db.session.add(admin)
db.session.commit()
The SonarQube scan was run again to verify that the issues were fixed.
Implement a JWT access and refresh token in your web application and document the example of token usage.
The JWT access and refresh tokens were implemented in the /login
route.
@app.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = User.query.filter_by(username=username, password=password).first()
if user:
access_token = create_access_token(identity=username)
refresh_token = create_refresh_token(identity=username)
response = jsonify(msg='Login Successful')
set_access_cookies(response, access_token)
set_refresh_cookies(response, refresh_token)
return response
return 'Login Failed', 401
...
The access token is used to access the /protected
route.
@app.route('/protected', methods=['GET'])
@jwt_required()
def protected():
current_user = get_jwt_identity()
return jsonify(logged_in_as=current_user), 200
The refresh token is used to refresh the access token.
@app.route("/refresh", methods=['POST', 'GET'])
@jwt_required(refresh=True)
def refresh():
identity = get_jwt_identity()
access_token = create_access_token(identity=identity)
response = make_response(jsonify(access_token=access_token))
set_access_cookies(response, access_token)
return response
Using the following configuration, the JWT tokens can be sent in the headers, cookies, JSON, or query string.
app.config["JWT_TOKEN_LOCATION"] = ["headers", "cookies", "json", "query_string"]
With the access token, the /protected
route can be accessed:
Without the access token, the /protected
route cannot be accessed:
Analyze the SQL injection vulnerability of your application with one of available tools online (for example: https://pentest-tools.com/website-vulnerability-scanning/sql-injection-scanner-online), document the potential bugs and describe the current way how the application protects it’s database from SQL injection attacks.
Since the application is running on a local server, online tools cannot be used to scan the application. However, the SQL injection vulnerability has been analyzed using the OWASP ZAP tool in Module 1.
To protect the database from SQL injection attacks, SQLAlchemy's ORM is used to interact with the database. This layer of abstraction ensures that the user input is sanitized and the queries are safe from SQL injection attacks.
Implement an example of serialization (if it does not exist in your application) and implement the deserialization protection based on whitelisting the classes that can be deserialized.
For this fifth module, the pickle
module was used to serialize and deserialize
the Foo
class.
class Foo:
attr1 = 1
attr2 = 'foo'
attr4 = [1, 2, 3]
attr5 = {'a': 1, 'b': 2, 'c': 3}
attr6 = None
types_whitelist = [int, str, list, dict] # , type(None)
# Route for deserializing data
@app.route('/deserialization', methods=['GET'])
def deserialization():
# Serialize data
serialized_data = pickle.dumps(Foo)
# Deserialize data
deserialized_data = pickle.loads(serialized_data)
# If an attribute is not in the whitelist, return an error
for k, v in deserialized_data.__dict__.items():
# Skip private attributes
if k.startswith('__'):
continue
if type(v) not in types_whitelist:
return jsonify(error=f"Invalid type: {type(v)}"), 500
return jsonify({
"className": deserialized_data.__name__,
"attributes": {k: v for k, v in deserialized_data.__dict__.items() if not k.startswith('__')}
}), 200
The Foo
class is serialized and deserialized using the pickle
module. The
deserialized data is checked against the whitelist of types. If the type of an
attribute is not in the whitelist, an error is returned.
Use the best practices in implementing authentication and authorization to prevent unauthorized access to confidential data.
The best practices in implementing authentication and authorization were used in
the application. The user credentials are stored in the database and the
passwords are hashed using the werkzeug.security
module.
from werkzeug.security import generate_password_hash, check_password_hash
...
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(200), nullable=False)
def init_db():
with app.app_context():
db.create_all()
if not User.query.filter_by(username=admin_username).first():
hashed_password = generate_password_hash(admin_password)
admin = User(username=admin_username, password=hashed_password)
db.session.add(admin)
db.session.commit()
...
@app.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = User.query.filter_by(username=username).first()
if user and check_password_hash(user.password, password):
access_token = create_access_token(identity=username)
refresh_token = create_refresh_token(identity=username)
response = jsonify(msg='Login Successful')
set_access_cookies(response, access_token)
set_refresh_cookies(response, refresh_token)
return response
return 'Login Failed', 401
...
The /protected
route is protected using the jwt_required
decorator as shown
in Module 3.
Hashing the passwords and using JWT tokens for authentication and authorization are the best practices to prevent unauthorized access to confidential data.