Skip to content

Commit beca461

Browse files
authored
Oas update (#55)
* Updated OAS to include auth and added swagger ui * custom 401 handler for connexion to match rest * update readme
1 parent 1449f9b commit beca461

File tree

8 files changed

+84
-62
lines changed

8 files changed

+84
-62
lines changed

Dockerfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
FROM python:3.7-alpine as builder
1+
FROM python:3.11-alpine as builder
22
RUN apk --update add bash nano g++
33
COPY . /vampi
44
WORKDIR /vampi
55
RUN pip install -r requirements.txt
66

77
# Build a fresh container, copying across files & compiled parts
8-
FROM python:3.7-alpine
8+
FROM python:3.11-alpine
99
COPY . /vampi
1010
WORKDIR /vampi
1111
COPY --from=builder /usr/local/lib /usr/local/lib

README.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ VAmPI is a vulnerable API made with Flask and it includes vulnerabilities from t
1313
- OpenAPI3 specs and Postman Collection included.
1414
- Global switch on/off to have a vulnerable environment or not.
1515
- Token-Based Authentication (Adjust lifetime from within app.py)
16+
- Available Swagger UI to directly interact with the API
1617

1718
VAmPI's flow of actions is going like this: an unregistered user can see minimal information about the dummy users included in the API. A user can register and then login to be allowed using the token received during login to post a book. For a book posted the data accepted are the title and a secret about that book. Each book is unique for every user and only the owner of the book should be allowed to view the secret.
1819

@@ -34,7 +35,7 @@ A quick rundown of the actions included can be seen in the following table:
3435
| POST | /books/v1 | Add new book |
3536
| GET | /books/v1/{book} | Retrieves book by title along with secret |
3637

37-
For more details you can use a service like the [swagger editor](https://editor.swagger.io) supplying it the OpenAPI specification which can be found in the directory `openapi_specs`.
38+
For more details you can either run VAmPI and visit `http://127.0.0.1:5000/ui/` or use a service like the [swagger editor](https://editor.swagger.io) supplying the OpenAPI specification which can be found in the directory `openapi_specs`.
3839

3940

4041
#### List of Vulnerabilities
@@ -70,6 +71,12 @@ docker run -p 5000:5000 erev0s/vampi:latest
7071
docker-compose up -d
7172
~~~~
7273

74+
## Available Swagger UI :rocket:
75+
Visit the path `/ui` where you are running the API and a Swagger UI will be available to help you get started!
76+
~~~~
77+
http://127.0.0.1:5000/ui/
78+
~~~~
79+
7380
## Customizing token timeout and vulnerable environment or not
7481
If you would like to alter the timeout of the token created after login or if you want to change the environment **not** to be vulnerable then you can use a few ways depending how you run the application.
7582

api_views/books.py

+5-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import jsonschema
22

3-
from api_views.users import token_validator
3+
from api_views.users import token_validator, error_message_helper
44
from config import db
55
from api_views.json_schemas import *
66
from flask import jsonify, Response, request, json
@@ -9,10 +9,6 @@
99
from app import vuln
1010

1111

12-
def error_message_helper(msg):
13-
return '{ "status": "fail", "message": "' + msg + '"}'
14-
15-
1612
def get_all_books():
1713
return_value = jsonify({'Books': Book.get_all_books()})
1814
return return_value
@@ -25,12 +21,10 @@ def add_new_book():
2521
except:
2622
return Response(error_message_helper("Please provide a proper JSON body."), 400, mimetype="application/json")
2723
resp = token_validator(request.headers.get('Authorization'))
28-
if "expired" in resp:
29-
return Response(error_message_helper(resp), 401, mimetype="application/json")
30-
elif "Invalid token" in resp:
24+
if "error" in resp:
3125
return Response(error_message_helper(resp), 401, mimetype="application/json")
3226
else:
33-
user = User.query.filter_by(username=resp).first()
27+
user = User.query.filter_by(username=resp['sub']).first()
3428

3529
# check if user already has this book title
3630
book = Book.query.filter_by(user=user, book_title=request_data.get('book_title')).first()
@@ -50,9 +44,7 @@ def add_new_book():
5044

5145
def get_by_title(book_title):
5246
resp = token_validator(request.headers.get('Authorization'))
53-
if "expired" in resp:
54-
return Response(error_message_helper(resp), 401, mimetype="application/json")
55-
elif "Invalid token" in resp:
47+
if "error" in resp:
5648
return Response(error_message_helper(resp), 401, mimetype="application/json")
5749
else:
5850
if vuln: # Broken Object Level Authorization
@@ -67,7 +59,7 @@ def get_by_title(book_title):
6759
else:
6860
return Response(error_message_helper("Book not found!"), 404, mimetype="application/json")
6961
else:
70-
user = User.query.filter_by(username=resp).first()
62+
user = User.query.filter_by(username=resp['sub']).first()
7163
book = Book.query.filter_by(user=user, book_title=str(book_title)).first()
7264
if book:
7365
responseObject = {

api_views/users.py

+19-21
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010

1111

1212
def error_message_helper(msg):
13-
return '{ "status": "fail", "message": "' + msg + '"}'
13+
if isinstance(msg, dict):
14+
return '{ "status": "fail", "message": "' + msg['error'] + '"}'
15+
else:
16+
return '{ "status": "fail", "message": "' + msg + '"}'
1417

1518

1619
def get_all_users():
@@ -81,12 +84,14 @@ def login_user():
8184
return Response(json.dumps(responseObject), 200, mimetype="application/json")
8285
if vuln: # Password Enumeration
8386
if user and request_data.get('password') != user.password:
84-
return Response(error_message_helper("Password is not correct for the given username."), 200, mimetype="application/json")
87+
return Response(error_message_helper("Password is not correct for the given username."), 200,
88+
mimetype="application/json")
8589
elif not user: # User enumeration
8690
return Response(error_message_helper("Username does not exist"), 200, mimetype="application/json")
8791
else:
8892
if (user and request_data.get('password') != user.password) or (not user):
89-
return Response(error_message_helper("Username or Password Incorrect!"), 200, mimetype="application/json")
93+
return Response(error_message_helper("Username or Password Incorrect!"), 200,
94+
mimetype="application/json")
9095
except jsonschema.exceptions.ValidationError as exc:
9196
return Response(error_message_helper(exc.message), 400, mimetype="application/json")
9297
except:
@@ -105,7 +110,7 @@ def token_validator(auth_header):
105110
# if auth_token is valid we get back the username of the user
106111
return User.decode_auth_token(auth_token)
107112
else:
108-
return "Invalid token"
113+
return {'error': 'Invalid token. Please log in again.'}
109114

110115

111116
def update_email(username):
@@ -115,12 +120,10 @@ def update_email(username):
115120
except:
116121
return Response(error_message_helper("Please provide a proper JSON body."), 400, mimetype="application/json")
117122
resp = token_validator(request.headers.get('Authorization'))
118-
if "expired" in resp:
119-
return Response(error_message_helper(resp), 401, mimetype="application/json")
120-
elif "Invalid token" in resp:
123+
if "error" in resp:
121124
return Response(error_message_helper(resp), 401, mimetype="application/json")
122125
else:
123-
user = User.query.filter_by(username=resp).first()
126+
user = User.query.filter_by(username=resp['sub']).first()
124127
if vuln: # Regex DoS
125128
match = re.search(
126129
r"^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@{1}([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})$",
@@ -137,7 +140,8 @@ def update_email(username):
137140
}
138141
return Response(json.dumps(responseObject), 204, mimetype="application/json")
139142
else:
140-
return Response(error_message_helper("Please Provide a valid email address."), 400, mimetype="application/json")
143+
return Response(error_message_helper("Please Provide a valid email address."), 400,
144+
mimetype="application/json")
141145
else:
142146
regex = '^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,3}$'
143147
if (re.search(regex, request_data.get('email'))):
@@ -152,16 +156,14 @@ def update_email(username):
152156
}
153157
return Response(json.dumps(responseObject), 204, mimetype="application/json")
154158
else:
155-
return Response(error_message_helper("Please Provide a valid email address."), 400, mimetype="application/json")
156-
159+
return Response(error_message_helper("Please Provide a valid email address."), 400,
160+
mimetype="application/json")
157161

158162

159163
def update_password(username):
160164
request_data = request.get_json()
161165
resp = token_validator(request.headers.get('Authorization'))
162-
if "expired" in resp:
163-
return Response(error_message_helper(resp), 401, mimetype="application/json")
164-
elif "Invalid token" in resp:
166+
if "error" in resp:
165167
return Response(error_message_helper(resp), 401, mimetype="application/json")
166168
else:
167169
if request_data.get('password'):
@@ -173,7 +175,7 @@ def update_password(username):
173175
else:
174176
return Response(error_message_helper("User Not Found"), 400, mimetype="application/json")
175177
else:
176-
user = User.query.filter_by(username=resp).first()
178+
user = User.query.filter_by(username=resp['sub']).first()
177179
user.password = request_data.get('password')
178180
db.session.commit()
179181
responseObject = {
@@ -185,16 +187,12 @@ def update_password(username):
185187
return Response(error_message_helper("Malformed Data"), 400, mimetype="application/json")
186188

187189

188-
189-
190190
def delete_user(username):
191191
resp = token_validator(request.headers.get('Authorization'))
192-
if "expired" in resp:
193-
return Response(error_message_helper(resp), 401, mimetype="application/json")
194-
elif "Invalid token" in resp:
192+
if "error" in resp:
195193
return Response(error_message_helper(resp), 401, mimetype="application/json")
196194
else:
197-
user = User.query.filter_by(username=resp).first()
195+
user = User.query.filter_by(username=resp['sub']).first()
198196
if user.admin:
199197
if bool(User.delete_user(username)):
200198
responseObject = {

config.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
import os
22
import connexion
3+
from flask import jsonify
34
from flask_sqlalchemy import SQLAlchemy
45

56
vuln_app = connexion.App(__name__, specification_dir='./openapi_specs')
67

7-
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(vuln_app.root_path, 'database/database.db')
8+
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(vuln_app.app.root_path, 'database/database.db')
89
vuln_app.app.config['SQLALCHEMY_DATABASE_URI'] = SQLALCHEMY_DATABASE_URI
910
vuln_app.app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
1011

1112
vuln_app.app.config['SECRET_KEY'] = 'random'
1213
# start the db
1314
db = SQLAlchemy(vuln_app.app)
1415

15-
vuln_app.add_api('openapi3.yml')
16+
17+
@vuln_app.app.errorhandler(401)
18+
def custom_401(error):
19+
# Custom 401 to match the original response sent by Vampi
20+
response = jsonify({"status": "fail", "message": "Invalid token. Please log in again."})
21+
response.status_code = 401
22+
return response
1623

1724

25+
vuln_app.add_api('openapi3.yml')

models/user_model.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from random import randrange
88
from sqlalchemy.sql import text
99

10+
1011
class User(db.Model):
1112
__tablename__ = 'users'
1213
id = db.Column(db.Integer, primary_key=True, unique=True, autoincrement=True)
@@ -45,17 +46,17 @@ def encode_auth_token(self, user_id):
4546
def decode_auth_token(auth_token):
4647
try:
4748
payload = jwt.decode(auth_token, vuln_app.app.config.get('SECRET_KEY'), algorithms=["HS256"])
48-
return payload['sub']
49+
return payload
4950
except jwt.ExpiredSignatureError:
50-
return 'Signature expired. Please log in again.'
51+
return {'error': 'Signature expired. Please log in again.'}
5152
except jwt.InvalidTokenError:
52-
return 'Invalid token. Please log in again.'
53+
return {'error': 'Invalid token. Please log in again.'}
5354

5455
def json(self):
55-
return{'username': self.username, 'email': self.email}
56+
return {'username': self.username, 'email': self.email}
5657

5758
def json_debug(self):
58-
return{'username': self.username, 'password': self.password, 'email': self.email, 'admin': self.admin}
59+
return {'username': self.username, 'password': self.password, 'email': self.email, 'admin': self.admin}
5960

6061
@staticmethod
6162
def get_all_users():

0 commit comments

Comments
 (0)